상세 컨텐츠

본문 제목

[IaC] Terraform을 이용하여 간편하게 3-Tier Test 환경 구축하기

Cloud Computing/1. AWS

by 채(彩) 2023. 9. 14. 11:39

본문

오늘은 Terraform을 이용하여 3-Tier Test 환경을 구축하는 방법을 알아보겠습니다.

 

우선 Terraform이 무엇인지 간단하게 알아보겠습니다.

Terraform은

  •  HashiCorp사가 만든 오픈 소스 "코드형 인프라" 툴입니다.
  • 개발자가 HCL(HashiCorp Configuration Language)이라고 불리는 언어를 사용하여 "최종 상태" 클라우드 또는 온프레미스 인프라를 작성하도록 합니다.
  • Provider를 통해 액세스 가능한 API로 많은 플랫폼 또는 서비스와 함께 작동합니다.
  • 변경 계획과 적용을 분리하여 변경할 내용을 적용할 때 발생할 수 있는 실수를 줄일 수 있습니다.
  • 여러 장소에 같은 구성의 인프라를 구축하고 변경할 수 있도록 자동화할 수 있습니다.
  • 인프라를 구축하는 데 드는 시간을 절약할 수 있고, 실수도 줄일 수 있습니다.

 

 

Terraform 설치

Terraform 다운로드 페이지에서 로컬 PC의 운영체제에 맞는 파일을 다운로드 합니다.이후 압축을 해제하고 원하는 경로에 삽입한 뒤, 환경설정에 해당 경로를 추가하면 설치가 완료됩니다.설치가 끝나면 로컬에 aws 인증 설정을 적용해 줍니다. AWS 계정에서 발급한 IAM 사용자의 AWS AccessKey와 Secret Access Key를 등록해줍니다. (aws cli가 설치되어 있어야 합니다.)

aws configure

 

 

Terraform 구성

1. Provider

Provider를 직역하면 제공자 입니다. 인프라의 타입이라고 생각하면 됩니다. 대표적인 Provider의 종류로는 AWS, GCP, Azure가 있습니다.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "us-east-1"
}

# Create a VPC
resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"
}

위 코드는 Terraform 공식 docs에 있는 사용 사례입니다.

terraform의 provider로 aws를 지정하고 provider의 version은 5.0버전 이상을 사용하겠다고 선언한 코드입니다.

이 때 provider의 설정을 작성해 주어야 하는데 하드코딩 하는 방법과 변수로 입력하는 방법이 있습니다.

(하드코딩 하는 방법은 보안상의 이유로 추천하지 않습니다.)

provider "aws" {
  region     = "us-west-2"
  access_key = "my-access-key"
  secret_key = "my-secret-key"
}

↑ 하드 코딩 하는 방법

↓ 하드 코딩을 하지 않는 방법 

- 이 경우에는 credential 파일에 정보를 저장하고 profile을 선택하는 방법입니다.

provider "aws" {
    region       = "ap-northeast-2"
    shared_credentials_file = "/Users/tf_user/.aws/creds" # Credential 파일 위치
    profile                 = "chaechae" # Credential 파일 내의 저장된 profile 중 선택
}

 

 

2. Resource Block

Resource Block은 아래와 같은 형태로 작성합니다.

resource "RESOURCE_TYPE" "NAME" {
  ARGUMENT = VALUE
}
  • RESOURCE_TYPE : terraform에서 제공하는 리소스의 종류를 작성합니다. (ex. aws_db_instance, aws_eip 등..)
  • NAME : 리소스 이름을 작성합니다. terraform 내에서 구분하기 위한 이름을 작성합니다.
  • ARGUMENT : 인자 or 속성 값입니다.

AWS provider의 Data Resource는 링크를 참고해 주시기 바랍니다.

예를 들면 아래와 같이 작성할 수 있습니다.

resource "aws_instance" "bastion_server" {
  ami           = "ami-23409c2"
  instance_type = "t3.medium"

  tags = {
    Name = "TestBastionServer"
  }
}

 

Terraform 실행 순서

1. 초기화 (init)

terraform init

Terraform을 initializing하는 명령어 입니다.

현재 터미널이 위치한 디렉토리에서 초기화를 진행하므로 Terraform main.tf파일이 있는 위치에서 진행해 줍니다.

초기화를 진행하게 되면 .terraform 폴더가 생성됩니다.

※ 이 폴더는 직접 수정하거나 혹은 공유하면 안됩니다.

  • 초기화를 실행하면 provider가 tf 코드를 받아서 api로 변경 후 실행해줍니다.
  • 초기화 작업은 [최초로 provider를 설치한 경우] 혹은 [provider 버전 업데이트]의 경우에 진행합니다.

 

2. 포멧팅(fmt)

terraform fmt

새로운 파일을 작성하거나 기존 파일을 변경 할 때 실행하는 명령어입니다. 생략할 수 있습니다.

 

 

3. 계획(Plan)

terraform plan

적용을 하기 전 실행하는 명령어로 terraform을 적용 했을 때의 출력을 검토할 수 있습니다.

소스의 양이 방대할 경우 [ -target=resource ] 옵션을 활용하여 해당 리소스와 의존관계에 있는 리소스만 검사 할 수 있습니다.

 

4. 적용(Apply)

terraform apply

Terraform을 provider에 적용 시키는 명령어 입니다. 이후 정말 승인 할지 한번 더 물어봅니다.

[ --auto-approve ] 옵션을 입력하면 자동으로 승인됩니다.

 

5. 삭제(destroy)

terraform destroy

provider에 배포된 Terraform의 리소스들을 삭제하는 명령어 입니다.

이후 정말 삭제할 것인지 묻는 터미널의 질문에 yes라고 입력하면 삭제 됩니다.

 

 

Terraform 변수 입력하기

Terraform은 변수를 사용할 때 따옴표를 사용하지 않습니다.

숫자를 사용할 때는 따옴표를 사용하지 않습니다.

예를들어 variable.tf 파일에 아래와 같이 입력합니다.

variable "instance_name" {
  type        = string
  description = "aws instance"
  default     = "WEB Server instance"
}

일반적으로 type, default, description 세가지를 정의합니다.

 


실습

오늘 실습에서 생성할 아키텍쳐는 위와 같습니다

Region(ap-northeast-2)에 새로운 VPC를 생성하고 2개의 AZ 위에 각각 Public, Private, Database Subnet을 만들것입니다.

Private Subnet에 있는 서버는 NAT Gateway를 통해 외부와 소통하고

Public Subnet에 있는 Bastion 서버는 Internet Gateway를 통해 외부와 소통합니다.

 

 

본격적으로 Terraform을 이용하여 3-tier test 환경을 구축해 보는 실습을 진행하겠습니다.

폴더 구조는 아래와 같습니다.

│   bastion.tf
│   iam.tf
│   output.tf
│   private_ec2.tf
│   rds.tf
│   security_groups.tf
│   variables.tf
│   vpc.tf
└───env
    └───dev
        │   main.tf
        │   variables.tf

.terraform 폴더와 provider 폴더 등등 우리가 작성하지 않아도 되는 폴더 구조는 생략하고

실습시 작성해야 할 tf 파일들만 나열해 보았습니다.

 

 

1. main.tf  (env/dev/)

terraform의 기본 골자가 되는 폴더입니다. Root module의 기본 진입점이며 Child Module의 Source위치 또는 Module들의 연결을 정의하는 파일이 됩니다.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-2"
}


module "dev" {
  source = "../../"

  # prj
  project_name = var.project_name
  enviroment = var.enviroment

  # VPC
  cidr_vpc = var.cidr_vpc
  cidr_public1 = var.cidr_public1
  cidr_public2 = var.cidr_public2
  cidr_private1 = var.cidr_private1
  cidr_private2 = var.cidr_private2
  cidr_rds2a = var.cidr_rds2a
  cidr_rds2c = var.cidr_rds2c

  # Public EC2

  bastion_instance_type = var.bastion_instance_type
  bastion_key_name      = var.bastion_key_name
  bastion_volume_size   = var.bastion_volume_size

  # Private EC2
  
  Private_EC2_instance_type = var.Private_EC2_instance_type
  Private_EC2_key_name      = var.Private_EC2_key_name
  Private_EC2_volume_size   = var.Private_EC2_volume_size
}
  • 최상단에서 provider를 aws로 지정합니다.
  • aws의 리전을 정의합니다
  • Cidr block, instance type, key name, volume size들을 정의합니다
  • 변수 값들을 일일히 작성해 주지 않고 variables.tf 파일로 관리하므로 var.$변수이름 으로 입력하여 변수 파일을 수정하여 활용성을 높입니다.
  • module dev에서는 main이 되는 소스가 ../../ 폴더에 있음을 선언합니다.

 

2. variables.tf (env/dev/)

# prj
variable "project_name" { default = "NAME" } 
variable "enviroment" { default = "ec2-test" }
variable "key_pair" { default = "INSERT YOUR KEYPAIR" }

# VPC
variable "cidr_vpc"        { default = "192.168.0.0/16"}
variable "cidr_public1"    { default = "192.168.1.0/24" }
variable "cidr_public2"    { default = "192.168.2.0/24" }

variable "cidr_private1"   { default = "192.168.10.0/24" }
variable "cidr_private2"   { default = "192.168.20.0/24" }

variable "cidr_rds2a"   { default = "192.168.100.0/24" }
variable "cidr_rds2c"   { default = "192.168.200.0/24" }
# Bastion

variable "bastion_instance_type" { default = "t3.medium" }
variable "bastion_key_name"      { default = "INSERT YOUR KEYPAIR" }
variable "bastion_volume_size"   { default = 10 }

# Private EC2

variable "Private_EC2_instance_type" { default = "t3.medium" }
variable "Private_EC2_key_name"      { default = "INSERT YOUR KEYPAIR" }
variable "Private_EC2_volume_size"   { default = 10 }
  • main이 되는 root module에서 사용할 변수값을 지정해 줍니다.
  • main.tf 에서 지정한 variable 변수들을 빠짐없이 입력해 줍니다.
  • 기본적으로 지정할 key pair에는 본인이 사용할 key pair name을 입력해 주도록 합니다.

 

여기서부터는 최상위 폴더에서 작업합니다.

main.tf 에서 source를 최상위 폴더로 지정했기 때문입니다.

3. vpc.tf (최상위 폴더)

# VPC
resource "aws_vpc" "vpc" {
  cidr_block           = var.cidr_vpc
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-${var.enviroment}-vpc"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project_name}-${var.enviroment}-igw"
  }
}

# NAT Gateway
resource "aws_nat_gateway" "nat_gateway" {
  allocation_id = aws_eip.nat_gateway.id
  subnet_id     = aws_subnet.public1.id
  depends_on    = [aws_internet_gateway.igw]

  tags = {
    Name = "${var.project_name}-${var.enviroment}-natgw1"
  }
}

resource "aws_eip" "nat_gateway" {
  vpc        = true
  depends_on = [aws_internet_gateway.igw]

  tags = {
    Name = "${var.project_name}-${var.enviroment}-natgw1-eip"
  }
}


# Default route table
resource "aws_default_route_table" "default" {
  default_route_table_id = aws_vpc.vpc.default_route_table_id

  tags = {
    Name = "${var.project_name}-${var.enviroment}-default-rtb"
  }
}

# Default security group
resource "aws_default_security_group" "default" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project_name}-${var.enviroment}-default-sg"
  }
}

# Default network access list
resource "aws_default_network_acl" "default" {
  default_network_acl_id = aws_vpc.vpc.default_network_acl_id

  tags = {
    Name = "${var.project_name}-${var.enviroment}-default-nacl"
  }
}

# Subnet
## public1-subnet
resource "aws_subnet" "public1" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-2a"
  cidr_block              = var.cidr_public1
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-${var.enviroment}-public1-subnet"
  }
}

## public2-subnet
resource "aws_subnet" "public2" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-2c"
  cidr_block              = var.cidr_public2
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-${var.enviroment}-public2-subnet"
  }
}



## private1-subnet
resource "aws_subnet" "private2a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-2a"
  cidr_block              = var.cidr_private1
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project_name}-${var.enviroment}-private2a-subnet"
  }
}

## private2-subnet
resource "aws_subnet" "private2c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-2c"
  cidr_block              = var.cidr_private2
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project_name}-${var.enviroment}-private2c-subnet"
  }
}

## RDS-subnet
resource "aws_subnet" "rds2a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-2a"
  cidr_block              = var.cidr_rds2a
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project_name}-${var.enviroment}-rds2a-subnet"
  }
}

resource "aws_subnet" "rds2c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-2c"
  cidr_block              = var.cidr_rds2c
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project_name}-${var.enviroment}-rds2c-subnet"
  }
}
# Route table
## public1~2
resource "aws_route_table" "public1" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project_name}-${var.enviroment}-public1-rtb"
  }
}

resource "aws_route_table_association" "public1" {
  subnet_id      = aws_subnet.public1.id
  route_table_id = aws_route_table.public1.id
}

resource "aws_route_table_association" "public2" {
  subnet_id      = aws_subnet.public2.id
  route_table_id = aws_route_table.public1.id
}

resource "aws_route" "public1" {
  route_table_id         = aws_route_table.public1.id
  gateway_id             = aws_internet_gateway.igw.id
  destination_cidr_block = "0.0.0.0/0"
}

## private1~2
resource "aws_route_table" "private2a" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project_name}-${var.enviroment}-private2a-rtb"
  }
}

resource "aws_route_table_association" "private2a" {
  subnet_id      = aws_subnet.private2a.id
  route_table_id = aws_route_table.private2a.id
}

resource "aws_route_table_association" "private2c" {
  subnet_id      = aws_subnet.private2c.id
  route_table_id = aws_route_table.private2a.id
}

resource "aws_route" "private2a" {
  route_table_id         = aws_route_table.private2a.id
  nat_gateway_id         = aws_nat_gateway.nat_gateway.id
  destination_cidr_block = "0.0.0.0/0"
}

## RDS 1~2
resource "aws_route_table" "rds2a" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project_name}-${var.enviroment}-rds-rtb"
  }
}
resource "aws_route_table" "rds2c" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project_name}-${var.enviroment}-rds-rtb"
  }
}

resource "aws_route_table_association" "rds2a" {
  subnet_id      = aws_subnet.rds2a.id
  route_table_id = aws_route_table.rds2a.id
}

resource "aws_route_table_association" "rds2c" {
  subnet_id      = aws_subnet.rds2c.id
  route_table_id = aws_route_table.rds2c.id
}



# NACL
## public1~2
resource "aws_network_acl" "public1" {
  vpc_id     = aws_vpc.vpc.id
  subnet_ids = [aws_subnet.public1.id, aws_subnet.public2.id]

  egress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  ingress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  tags = {
    Name = "${var.project_name}-${var.enviroment}-public-nacl"
  }
}

## private1~2
resource "aws_network_acl" "private2a" {
  vpc_id     = aws_vpc.vpc.id
  subnet_ids = [aws_subnet.private2a.id, aws_subnet.private2c.id]

  egress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  ingress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  tags = {
    Name = "${var.project_name}-${var.enviroment}-private1-nacl"
  }
}
  • vpc의 설정값들을 정의하고 입력해 줍니다.
  • 기본적으로는 vpc module을 사용하지만, 이번 실습에선 기초적인 이해부터 진행하기 위해 resource 타입으로 정의하겠습니다.
  • vpc의 cidr block, internet gateway, nat gateway, eip, route table, security group, NACL, subnet 등을 정의해줍니다.
  • 다음으로는 여기에 사용되는 variables를 살펴보겠습니다.

 

4. variables.tf (최상위 폴더)

# prj
variable "project_name" {
  default = "charon"
  type    = string
}
variable "enviroment" {
  default = "ec2-test"
}
# network
variable "cidr_vpc" {
  default = "192.168.0.0/16"
}
variable "cidr_public1" {
  default = "192.168.1.0/24"
}
variable "cidr_public2" {
  default = "192.168.2.0/24"
}


variable "cidr_private1" {
  default = "192.168.10.0/24"
}
variable "cidr_private2" {
  default = "192.168.20.0/24"
}

variable "cidr_rds2a" {
  default = "192.168.100.0/24"
}
variable "cidr_rds2c" {
  default = "192.168.200.0/24"
}


# Bastion AMI를 미리 콘솔에서 생성해 주어야 합니다.
variable "bastion_ami" {
  default = "ami-0fe176a332565f0d4"
}
variable "bastion_instance_type" {
  default = "t3.medium"
}
variable "bastion_key_name" {
  default = "INSERT YOUT KEYPAIR"
}
variable "bastion_volume_size" {
  default = "10"
}
variable "bastion_volume_type" {
  default = "gp3"
}

# Private EC2
variable "Private_EC2_ami" {
  default = "ami-0fe176a332565f0d4"
}

variable "Private_EC2_instance_type" {
  default = "t3.medium"
}
variable "Private_EC2_key_name" {
  default = "INSERT YOUT KEYPAIR"
}
variable "Private_EC2_volume_size" {
  default = "10"
}
variable "Private_EC2_volume_type" {
  default = "gp3"
}

#Access Key 아래와 같이 하드코딩 하는 방식은 추천하지 않습니다.
variable "AWS_AccessKey" {
  default = "INSERT YOUR ACCESSKEY"
}

variable "AWS_SecretKey" {
  default = "INSERT YOUR SECRETKEY"
}


# DB password
variable "db_password" {
  description = "RDS root user password"
  type        = string
  sensitive   = true
}
  • vpc 및 다른 tf 파일들에 사용할 변수값들을 정의해줍니다.
  • (주의) AMI는 미리 콘솔에서 생성해 주어야 합니다.

  • 이 실습에서는 편의를 위해 Access key와 Secret Key를 하드코딩 했으나 앞서 설명드린 것 처럼 보안을 위해 추천하지 않는 방식입니다.

 

5. security_groups.tf

# Security Group
# Bastion EC2 SG
resource "aws_security_group" "bastion_ec2" {
  name        = "${var.project_name}-${var.enviroment}-bastion-sg"
  description = "for bastion ec2"
  vpc_id      = aws_vpc.vpc.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-${var.enviroment}-bastion-sg"
  }
}


# Private EC2 SG
resource "aws_security_group" "private_ec2" {
  name        = "${var.project_name}-${var.enviroment}-private-sg"
  description = "for private ec2"
  vpc_id      = aws_vpc.vpc.id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-${var.enviroment}-private-sg"
  }
}

resource "aws_security_group_rule" "private_ec2" {
  type                     = "ingress"
  from_port                = 22
  to_port                  = 22
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.bastion_ec2.id
  security_group_id        = aws_security_group.private_ec2.id
}

# RDS SG

resource "aws_security_group" "private_rds" {
  name        = "${var.project_name}-${var.enviroment}-rds-sg"
  description = "for private rds"
  vpc_id      = aws_vpc.vpc.id

  ingress {
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = [aws_security_group.bastion_ec2.id]
  }

  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.private_ec2.id]
  }
  egress {
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = [aws_security_group.bastion_ec2.id]
  }

  egress {
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-${var.enviroment}-rds-sg"
  }
}
  • 보안그룹을 설정합니다.
  • 각각 서브넷에 맞는 보안그룹을 입맛에 맞게 변경해줍니다.

 

6. iam.tf

data "aws_iam_policy_document" "ec2_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

data "aws_iam_policy" "systems_manager" {
  arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

data "aws_iam_policy" "cloudwatch_agent" {
  arn = "arn:aws:iam::aws:policy/CloudWatchAgentAdminPolicy"
}

# IAM Role
## bastion
resource "aws_iam_role" "bastion" {
  name               = "${var.project_name}-${var.enviroment}-bastion-iamrole"
  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}

resource "aws_iam_role_policy_attachment" "bastion_ssm" {
  role       = aws_iam_role.bastion.name
  policy_arn = data.aws_iam_policy.systems_manager.arn
}

resource "aws_iam_role_policy_attachment" "bastion_cloudwatch" {
  role       = aws_iam_role.bastion.name
  policy_arn = data.aws_iam_policy.cloudwatch_agent.arn
}

resource "aws_iam_instance_profile" "bastion" {
  name = "${var.project_name}-${var.enviroment}-bastion-instanceprofile"
  role = aws_iam_role.bastion.name
}

## private_ec2
resource "aws_iam_role" "private_ec2" {
  name               = "${var.project_name}-${var.enviroment}-private_ec2-iamrole"
  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}

resource "aws_iam_role_policy_attachment" "private_ec2_ssm" {
  role       = aws_iam_role.private_ec2.name
  policy_arn = data.aws_iam_policy.systems_manager.arn
}

resource "aws_iam_role_policy_attachment" "private_ec2_cloudwatch" {
  role       = aws_iam_role.private_ec2.name
  policy_arn = data.aws_iam_policy.cloudwatch_agent.arn
}

resource "aws_iam_instance_profile" "private_ec2" {
  name = "${var.project_name}-${var.enviroment}-private_ec2-instanceprofile"
  role = aws_iam_role.private_ec2.name
}
  • systems manager, cloudwatch agent, bastion server, private server에 필요한 iam들의 설정을 작성해줍니다.

 

7. Output.tf

output "rds_hostname" {
  description = "RDS instance hostname"
  value       = aws_db_instance.charon
  sensitive   = true
}

output "rds_port" {
  description = "RDS instance port"
  value       = aws_db_instance.charon.port
  sensitive   = true
}

output "rds_username" {
  description = "RDS instance root username"
  value       = aws_db_instance.charon.username
  sensitive   = true
}

 

  • Output이란 작성한 인프라에 대한 정보를 명령줄에서 사용할 수 있도록 하며 사용할 다른 Terraform 구성에 대한 정보를 노출해 주는 파일입니다. 이 실습에선 간단하게 RDS의 정보에 대해서만 노출해 보겠습니다.

배포가 완료되면 이렇게 output이 출력됩니다.

 

이제까지 VPC를 구성하는 환경에 대해서 terraform 코드를 통해 설정하였습니다. 다음으로는 bastion ec2, private ec2, rds를 생성하는 작업을 진행해 보겠습니다.

8. bastion.tf

# EC2
resource "aws_eip" "bastion" {
  instance = aws_instance.bastion.id
  vpc      = true
  tags = {
    Name = "${var.project_name}-${var.enviroment}-bastion-eip"
  }
}

resource "aws_instance" "bastion" {
  ami                     = var.bastion_ami
  instance_type           = var.bastion_instance_type
  vpc_security_group_ids  = [aws_security_group.bastion_ec2.id]
  iam_instance_profile    = aws_iam_instance_profile.bastion.name
  subnet_id               = aws_subnet.public1.id
  key_name                = var.bastion_key_name
  disable_api_termination = true
  root_block_device {
    volume_size           = var.bastion_volume_size
    volume_type           = "gp3"
    delete_on_termination = true
    tags = {
      Name = "${var.project_name}-${var.enviroment}-bastion-ec2"
    }
  }
  tags = {
    Name = "${var.project_name}-${var.enviroment}-bastion-ec2"
  }
}
  • terraform에서 작성하는 resource에 대해서는 사용할 수 있는 argument가 정해져 있습니다. (terraform docs참조)
  • 이 bastion.tf 에서는 eip를 정의하고, 앞서 지정된 ami를 통해 bastion host를 생성합니다.

 

 

9. private_ec2

# EC2
resource "aws_instance" "private-ec2" {
  ami                         = var.Private_EC2_ami
  instance_type               = var.Private_EC2_instance_type
  vpc_security_group_ids      = [aws_security_group.private_ec2.id]
  iam_instance_profile        = aws_iam_instance_profile.private_ec2.name
  subnet_id                   = aws_subnet.private2a.id
  associate_public_ip_address = false
  key_name                    = var.Private_EC2_key_name
  disable_api_termination     = true
  root_block_device {
    volume_size           = var.Private_EC2_volume_size
    volume_type           = "gp3"
    delete_on_termination = true
    tags = {
      Name = "${var.project_name}-${var.enviroment}-private-ec2"
    }
  }
  tags = {
    Name = "${var.project_name}-${var.enviroment}-private-ec2"
  }
}
  • private ec2의 경우에는 외부로 노출할 eip가 없으므로 instance만 정의해줍니다.

 

10. RDS (mysql)

resource "aws_db_parameter_group" "charon" {
  name   = "charon"
  family = "mysql5.7"

  parameter {
    name  = "character_set_server"
    value = "utf8"
  }

  parameter {
    name  = "character_set_client"
    value = "utf8"
  }
}

#DB subnet group
resource "aws_db_subnet_group" "charon" {
  name       = "charon"
  subnet_ids = [aws_subnet.rds2a.id, aws_subnet.rds2c.id]
}



resource "aws_db_instance" "charon" {
  identifier             = "charon"
  allocated_storage      = 10
  db_name                = "charon_rds"
  engine                 = "mysql"
  engine_version         = "5.7"
  instance_class         = "db.t3.medium"
  username               = "charon"
  password               = "INSERT YOUR PASSWORD"
  parameter_group_name   = aws_db_parameter_group.charon.name
  skip_final_snapshot    = true
  publicly_accessible    = true
  vpc_security_group_ids = [aws_security_group.private_rds.id]
  db_subnet_group_name   = aws_db_subnet_group.charon.name
}
  • RDS를 정의해 줍니다. password는 독자분들의 비밀번호를 치환하여 작성해주시면 되겠습니다.

 

이로써 드디어 tf 코드 작성이 완료되었습니다.


배포

 

이제 배포를 시작해 보겠습니다.

필자는 VS code환경에서 코드를 작성하였으므로 참고하여 주시기 바랍니다.

터미널을 열고 해당 폴더로 진입합니다. 이후 terraform init을 하여 terraform의 initializing을 진행해줍니다.

Initializing이 성공하면 위와 같은 메세지가 출력됩니다.

현재는 변경 사항이 없으므로 포멧팅은 생략합니다.

모든 파일에 대해서 배포를 진행할 것이므로 특정 옵션을 사용하지 않고 계획을 진행하겠습니다.

terraform plan을 입력하여 문법과 배포 등에 이상이 없는지 확인합니다.

 

앞서 작성한 RDS root user의 비밀번호를 입력해 줍니다.

Plan이 성공하면 위와 같은 메세지가 출력되고 앞서 작성한 Outputs 파일의 값들이 표시됩니다.

Output의 정보를 더 출력받기를 원하신다면 독자분들의 입맛에 맞게 코드를 추가해 주면 되겠습니다.

 

이제 terraform apply를 진행해 보겠습니다.

앞서 plan과 같이 RDS 비밀번호를 물어봅니다. 

이후 비밀번호를 입력해 주면 실제로 배포를 진행할 지 다시한번 물어봅니다.

yes를 입력하면 배포가 진행됩니다.

대다수의 리소스들이 1초 이내로 배포 됩니다.

다만, nat gateway와 RDS는 조금 소요되지만 모든 리소스가 생성되는데 5분 가량이면 완료됩니다.

콘솔에서 직접 VPC를 만들고 기타 설정들을 한땀한땀 진행하는 것 보다 수십배나 절약할 수 있습니다.

 

저는 모든 리소스가 생성되는데 6분정도 소요되었습니다.

그럼 이제 배포된 리소스들을 확인해 보겠습니다.

 

VPC
Routing Table
EC2
RDS

 

모든 리소스들이 정상적으로 생성되었습니다.

 

이로써 Terraform을 이용하여 3tier test 환경을 구축하는 실습이 완료 되었습니다

마지막으로 리소스들을 삭제하는 destroy 명령어를 입력하여 모든 리소스를 삭제해보겠습니다.

 

앞서와 같이 RDS의 비밀번호를 입력해줍니다.

리소스를 진짜 삭제할지 물어봅니다. yes를 입력해 줍니다.

성공했다면 아래와 같은 메세지가 출력됩니다.

소스코드가 변경되지 않은 이상 destroy가 실패할 경우는 없습니다.

default ACL은 삭제되지 않습니다.

 

이로써 모든 실습이 완료되었습니다.

감사합니다.

관련글 더보기

댓글 영역