테라폼으로 AWS 환경 구축하기 2장. ALB EC2 RDS S3 구축하기 – 1

초심자도 이해할 수 있도록 테라폼을 이용하여 EC2 RDS S3의 구성을 작성해보는 글입니다.
2021.10.29

안녕하세요 클래스메소드의 이수재입니다.

요즘 테라폼을 전혀 공부하지 않았어서 사용법을 거의 잊어버렸습니다...
기억도 되살릴겸 다시금 테라폼 블로그를 작성하려 합니다.

무엇을 하는지

이번 주제에선 간단한 내용의 구성을 테라폼으로 구축해보며 감을 잡아봅니다.
이후 불편한 점이나 개선점 등을 알아보고 이러한 부분을 어떻게 개선할 수 있는지 알아봅니다.

설치 방법에 대해 예전에 작성한 글이나 공식 페이지를 참고하여 테라폼을 설치해주세요.

환경

  • terraform 1.0.9
  • tfswitch 0.12.1168
  • AWS Vault 6.3.1-Homebrew

당연하지만 aws 계정 생성과 cli의 인스톨은 미리 되어 있어야 합니다.
또한 해당 글에서는 aws vault를 사용하지만 일반 aws cli 환경이라도 상관없습니다.
aws vault 에 대한 자세한 내용은 이 글을 참고해주세요.

이 글에선 간단하게 다음과 같은 구성을 테라폼으로 작성합니다.


작성되는 주요 리소스는 다음과 같습니다.

  • network
    • vpc
    • subnet(public X 2, private X 4)
    • vpc endpoint
    • security group X 2
  • EC2 X 2
    • ALB X 1
  • RDS

해당 글에서 사용한 템플릿은 깃 허브에 업로드되어 있으니 참고해주세요.

작성하기

전체적인 흐름은 다음과 같습니다.

  1. 프로바이더 설정
  2. 네트워크 리소스 작성
  3. EC2 리소스 작성
  4. RDS 리소스 작성
  5. S3 리소스 작성

각 단계의 템플릿을 보며 이해해보겠습니다.

프로바이더 설정

우선 사용할 테라폼의 버전과 aws 의 버전을 지정해줍니다.

# mail.tf
terraform {
  required_version = "1.0.9"

  required_providers {
    aws = "~> 3.0"
  }
}

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

이후 해당 디렉토리에서 tfswitch를 해주시면 tfswitch가 버전에 맞춰 테라폼을 지정해줍니다.

$ tfswitch
Reading required version from terraform file
Reading required version from constraint: 1.0.9
Matched version: 1.0.9
Switched terraform to version "1.0.9"

제 환경에는 이미 설치가 되어 있기 때문에 위와 같은 문구가 나오지만 1.0.9 버전이 설치되어 있지 않다면 설치 후 버전이 변경됩니다.
현재 aws vault를 쓰고 있기 때문에 provider 블록에는 region만 설정되어 있습니다.
이 내용도 환경 변수로 지정하면 없어도 될 내용이지만 어떠한 내용이 설정되는지 표시하기 위해 설정하였습니다.
프로바이더에 대한 자세한 내용은 이전 글을 참고해주세요.

네트워크 리소스

이어서 VPC, Subnet, VPC endpoint, Internet Gateway, Security Group, Route table 등을 작성합니다.

# VPC
resource "aws_vpc" "test" {
  cidr_block = "159.0.0.0/16"
  tags = {
    "Name" = "terraform-test-vpc"
  }
}

# VPC Endpoint
resource "aws_vpc_endpoint" "s3" {
  vpc_id = aws_vpc.test.id
  service_name = "com.amazonaws.ap-northeast-1.s3"
}

# Subnets
## public subnet
resource "aws_subnet" "publicSubnet1" {
  vpc_id = aws_vpc.test.id
  cidr_block = "159.0.0.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    "Name" = "test-public-subnet-01"
  }
}

resource "aws_subnet" "publicSubnet2" {
  vpc_id = aws_vpc.test.id
  cidr_block = "159.0.1.0/24"
  availability_zone = "ap-northeast-1c"
  tags = {
    "Name" = "test-public-subnet-02"
  }
}

## private ec2 subnet
resource "aws_subnet" "privateEC2Subnet1" {
  vpc_id = aws_vpc.test.id
  cidr_block = "159.0.2.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    "Name" = "test-private-ec2-subnet-01"
  }
}

resource "aws_subnet" "privateEC2Subnet2" {
  vpc_id = aws_vpc.test.id
  cidr_block = "159.0.3.0/24"
  availability_zone = "ap-northeast-1c"
  tags = {
    "Name" = "test-private-ec2-subnet-02"
  }
}

## private rds subnet
resource "aws_subnet" "privateRDSSubnet1" {
  vpc_id = aws_vpc.test.id
  cidr_block = "159.0.4.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    "Name" = "test-private-rds-subnet-01"
  }
}

resource "aws_subnet" "privateRDSSubnet2" {
  vpc_id = aws_vpc.test.id
  cidr_block = "159.0.5.0/24"
  availability_zone = "ap-northeast-1c"
  tags = {
    "Name" = "test-private-rds-subnet-02"
  }
}

# IGW
resource "aws_internet_gateway" "testIGW" {
  vpc_id = aws_vpc.test.id
  tags = {
    "Name" = "testIGW"
  }
}

# Route Table
resource "aws_route_table" "testPublicRTb" {
  vpc_id = aws_vpc.test.id


  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.testIGW.id
  }

  tags = {
    "Name" = "test-public-rtb"
  }
}

resource "aws_route_table" "testPrivateRTb" {
  vpc_id = aws_vpc.test.id
  tags = {
    "Name" = "test-private-rtb"
  }
}

## route
### vpc endpoint associate
resource "aws_vpc_endpoint_route_table_association" "publicRTbEndpoint" {
  route_table_id = aws_route_table.testPublicRTb.id
  vpc_endpoint_id = aws_vpc_endpoint.s3.id
}

resource "aws_vpc_endpoint_route_table_association" "privateRTbEndpoint" {
  route_table_id = aws_route_table.testPrivateRTb.id
  vpc_endpoint_id = aws_vpc_endpoint.s3.id
}

### subnet associate
resource "aws_route_table_association" "publicRTbAssociation01" {
  subnet_id = aws_subnet.publicSubnet1.id
  route_table_id = aws_route_table.testPublicRTb.id
}

resource "aws_route_table_association" "publicRTbAssociation02" {
  subnet_id = aws_subnet.publicSubnet2.id
  route_table_id = aws_route_table.testPublicRTb.id
}

resource "aws_route_table_association" "privateRTbAssociation01" {
  subnet_id = aws_subnet.privateEC2Subnet1.id
  route_table_id = aws_route_table.testPrivateRTb.id
}

resource "aws_route_table_association" "privateRTbAssociation02" {
  subnet_id = aws_subnet.privateEC2Subnet2.id
  route_table_id = aws_route_table.testPrivateRTb.id
}

resource "aws_route_table_association" "privateRTbAssociation03" {
  subnet_id = aws_subnet.privateRDSSubnet1.id
  route_table_id = aws_route_table.testPrivateRTb.id
}

resource "aws_route_table_association" "privateRTbAssociation04" {
  subnet_id = aws_subnet.privateRDSSubnet2.id
  route_table_id = aws_route_table.testPrivateRTb.id
}

소스가 길어보이지만 내용 자체는 반복이 많습니다.
반복되는 내용은 많은데 소스가 길어지는 것은 아무래도 한눈에 봐도 이해가 되지 않고 비효율적이라고 생각됩니다.
이번 글에서는 이러한 개선할 점을 하나하나 알아가며 다음에 개선할 방법을 생각해보는 것이 목적이기 때문에 우선 이렇게 작성합니다.

여기까지 작성이 완료되었다면 해당 템플릿으로 어떠한 리소스가 작성되는지 확인하기 위해 plan을 해봅니다.
CloudFormation의 create-change-set , execute-change-set 조합과 같습니다.
단순히 terraform 만을 사용한다면 terraform plan로 확인할 수 있지만 이 글에서는 aws vault를 사용하기 때문에 다음과 같은 명령어로 실행합니다.

$ aws-vault exec test-sujae -- terraform plan
Enter token for arn:aws:iam::-----:mfa/++++: 

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:
...
Plan: 19 to add, 0 to change, 0 to destroy.

plan을 보니 예상대로 잘 완성된 것 같습니다.

Security Group

이어서 security group을 작성합니다.

# Security Group
## public Seucurity Group
resource "aws_security_group" "publicSG01" {
  name = "public-SG-01"
  description = "Allow all HTTP"
  vpc_id = aws_vpc.test.id
  ingress {
    cidr_blocks = [ "0.0.0.0/0" ]
    from_port = 80
    protocol = "tcp"
    to_port = 80
  }

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

## private Security Group
resource "aws_security_group" "privateEC2SG01" {
  name = "private-ec2-sg-01"
  description = "Allow HTTP from ALB"
  vpc_id = aws_vpc.test.id
  ingress = [ {
    cidr_blocks = null
    description = null
    from_port = 80
    ipv6_cidr_blocks = null
    prefix_list_ids = null
    protocol = "tcp"
    security_groups = [ aws_security_group.publicSG01.id ]
    self = false
    to_port = 80
  } ]

  egress = [ {
    cidr_blocks = [ "0.0.0.0/0" ]
    description = null
    from_port = 0
    ipv6_cidr_blocks = null
    prefix_list_ids = null
    protocol = "-1"
    security_groups = null
    self = false
    to_port = 0
  } ]
}

ALB용 public security group과 ec2용 security group 을 만들어 줍니다.
마찬가지로 작성 후 plan을 해봅니다.
문제가 없다면 지금까지 작성한 리소스를 apply하여 리소스를 작성해봅니다.

$ aws-vault exec test-sujae -- terraform destroy

모든 리소스의 이미지를 넣기에는 양이 너무 많아 VPC만 이미지를 첨부하였습니다.
VPC 이외에도 다양한 리소스들이 작성된 것을 확인할 수 있습니다.

리소스가 생성된 것을 확인하였다면 destroy로 생성한 리소스를 삭제해둡니다.

$ aws-vault exec test-sujae -- terraform destroy

우선 여기까지

코드를 붙여넣다보니 역시 글이 너무 길어져버렸네요...
작성하는 리소스가 많지 않기 때문에 각 리소스 타입에 맞는 템플릿을 작성하여 관리할 수 있었습니다.
하지만 작성해야하는 리소스의 양이 더 많아지고 템플릿 안에서 반복되는 부분이 많아진다면 지금과 같은 방법으로 관리할 수 있을까요?

다음 글에서 나머지 리소스를 작성해보고 불편했던 점과 개선 방법 등을 알아봅니다.

긴 글 읽어주셔서 감사합니다.
오탈자 및 내용 피드백은 언제나 환영합니다. must01940 지메일로 보내주면 감사합니다.