테라폼(Terraform)으로 EC2 Instance 생성해 보기

이번에는 Terraform으로 EC2 Instance 생성해 보는 방법 대해서 정리해 봤습니다.
2022.06.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

안녕하세요 클래스메소드 김재욱(Kim Jaewook) 입니다. 이번에는 Terraform으로 EC2 Instance 생성해 보는 방법 대해서 정리해 봤습니다.

Terraform에 대한 기본 설정은 아래 블로그를 참고해 주세요.

디렉토리 구성


$ tree

├── bastion.tf
├── private.tf
├── iam.tf
├── security_groups.tf
├── variables.tf
├── vpc.tf
    └── env
        └── dev
              ├── main.tf
              └── variables.tf

현재 디렉토리 구성은 다음과 같습니다.

Public, Private Subnet에 각각의 EC2 인스턴스를 생성하고, SSM으로 접속하기 위해서 IAM Role을 생성합니다.

dev폴더에 있는 main,tf와 variables.tf 를 통해 전체적인 리소스를 관리합니다.

Variables 생성

variables.tf
# prj
variable "project_name" {} 
variable "environment" {}

# network
variable "cidr_vpc" {}
variable "cidr_public1" {}
variable "cidr_public2" {}
variable "cidr_public3" {}
variable "cidr_public4" {}
variable "cidr_private1" {}
variable "cidr_private2" {}
variable "cidr_private3" {}
variable "cidr_private4" {}

# Bastion
variable "bastion_ami" {}
variable "bastion_instance_type" {}
variable "bastion_key_name" {}
variable "bastion_volume_size" {}

# Private EC2
variable "Private_EC2_ami" {}
variable "Private_EC2_instance_type" {}
variable "Private_EC2_key_name" {}
variable "Private_EC2_volume_size" {}

먼저 Variables를 통해 각각 사용할 변수들을 선언합니다.

IAM Role 생성

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.environment}-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.environment}-bastion-instanceprofile"
  role = aws_iam_role.bastion.name
}

## private_ec2
resource "aws_iam_role" "private_ec2" {
  name               = "${var.project_name}-${var.environment}-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.environment}-private_ec2-instanceprofile"
  role = aws_iam_role.private_ec2.name
}

IAM Role은 Bastion EC2에서 사용할 Role과 Private EC2에서 사용할 Role 2개를 생성합니다.

각각의 Role에는 AmazonSSMManagedInstanceCore 정책을 추가해서 SSM으로 접속이 가능하게 합니다.

VPC 생성

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.environment}-vpc"
  }
}

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

  tags = {
    Name = "${var.project_name}-${var.environment}-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.environment}-natgw1"
  }
}

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

  tags = {
    Name = "${var.project_name}-${var.environment}-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.environment}-default-rtb"
  }
}

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

  tags = {
    Name = "${var.project_name}-${var.environment}-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.environment}-default-nacl"
  }
}

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

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

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

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

## public3-subnet
resource "aws_subnet" "public3" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1a"
  cidr_block              = "${var.cidr_public3}"
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project_name}-${var.environment}-public3-subnet"
  }
}

## public4-subnet
resource "aws_subnet" "public4" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1c"
  cidr_block              = "${var.cidr_public4}"
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project_name}-${var.environment}-public4-subnet"
  }
}

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

  tags = {
    Name = "${var.project_name}-${var.environment}-private1-subnet"
  }
}

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

  tags = {
    Name = "${var.project_name}-${var.environment}-private2-subnet"
  }
}

## private3-subnet
resource "aws_subnet" "private3" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1a"
  cidr_block              = "${var.cidr_private3}"
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project_name}-${var.environment}-private3-subnet"
  }
}

## private4-subnet
resource "aws_subnet" "private4" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1c"
  cidr_block              = "${var.cidr_private4}"
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project_name}-${var.environment}-private4-subnet"
  }
}


# Route table
## public1~2
resource "aws_route_table" "public1" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project_name}-${var.environment}-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"
}

## public3~4
resource "aws_route_table" "public3" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project_name}-${var.environment}-public3-rtb"
  }
}

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

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

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

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

  tags = {
    Name = "${var.project_name}-${var.environment}-private1-rtb"
  }
}

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

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

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

## private3~4
resource "aws_route_table" "private3" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project_name}-${var.environment}-private3-rtb"
  }
}

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

resource "aws_route_table_association" "private4" {
  subnet_id      = aws_subnet.private4.id
  route_table_id = aws_route_table.private3.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.environment}-public-nacl"
  }
}

## public3~4
resource "aws_network_acl" "public3" {
  vpc_id     = aws_vpc.vpc.id
  subnet_ids = [aws_subnet.public3.id, aws_subnet.public4.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.environment}-public3-nacl"
  }
}

## private1~2
resource "aws_network_acl" "private1" {
  vpc_id     = aws_vpc.vpc.id
  subnet_ids = [aws_subnet.private1.id, aws_subnet.private2.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.environment}-private1-nacl"
  }
}

## private3~4
resource "aws_network_acl" "private3" {
  vpc_id     = aws_vpc.vpc.id
  subnet_ids = [aws_subnet.private3.id, aws_subnet.private4.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.environment}-private3-nacl"
  }
}

VPC에서는 Public Subnet, Private Subnet 각각 3개씩 생성하고, NAT Gateway를 생성합니다.

본인 환경에따라 유연하게 Subnet을 수정해주시면 될 것 같습니다.

Security Groups 생성

security_groups.tf

# Security Group
# Bastion EC2 SG
resource "aws_security_group" "bastion_ec2"{
    name        = "${var.project_name}-${var.environment}-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"]
  }

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

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

# Private EC2 SG
resource "aws_security_group" "private_ec2"{
    name        = "${var.project_name}-${var.environment}-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.environment}-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
}

Security Group에서는 Bastion EC2와 Private EC2에서 사용할 Security Group만 생성합니다.

Private EC2은 Bastion EC2만 접속할 수 있도록 하기 위해서, 인바운드 룰에 Bastion EC2의 Security Group만 적용해 놓은 상태입니다.

Bastion EC2 생성

bastion.tf
# EC2
resource "aws_eip" "bastion" {
  instance = aws_instance.bastion.id
  vpc      = true
  tags = {
    Name = "${var.project_name}-${var.environment}-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.environment}-bastion-ec2"
    }
  }
  tags = {
    Name = "${var.project_name}-${var.environment}-bastion-ec2"
  }
}

Bastion EC2에는 EIP를 적용해서 생성했습니다.

Private EC2 생성

private.tf
# 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.private1.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.environment}-private-ec2"
    }
  }
  tags = {
    Name = "${var.project_name}-${var.environment}-private-ec2"
  }
}

Private EC2에서는 EIP를 할당하지 않은 상태로 생성했으며, associate_public_ip_address = false를 통해서 Public IP 할당을 없앴습니다.

Main Variables 생성

env/dev/variables.tf
# prj
variable "project_name" { default = "test" } 
variable "environment" { default = "dev" }
variable "key_name" { default = "tokyo-ec2-key" }

# VPC
variable "cidr_vpc"        { default = "10.0.0.0/16"}
variable "cidr_public1"    { default = "10.0.0.0/24" }
variable "cidr_public2"    { default = "10.0.1.0/24" }
variable "cidr_public3"    { default = "10.0.2.0/24" }
variable "cidr_public4"    { default = "10.0.3.0/24" }
variable "cidr_private1"   { default = "10.0.11.0/24" }
variable "cidr_private2"   { default = "10.0.12.0/24" }
variable "cidr_private3"   { default = "10.0.13.0/24" }
variable "cidr_private4"   { default = "10.0.14.0/24" }

# Bastion
variable "bastion_ami"           { default = "ami-02c3627b04781eada" }
variable "bastion_instance_type" { default = "t3.micro" }
variable "bastion_key_name"      { default = "tokyo-ec2-key" }
variable "bastion_volume_size"   { default = 8 }

# Private EC2
variable "Private_EC2_ami"           { default = "ami-02c3627b04781eada" }
variable "Private_EC2_instance_type" { default = "t3.micro" }
variable "Private_EC2_key_name"      { default = "tokyo-ec2-key" }
variable "Private_EC2_volume_size"   { default = 8 }

다음 env/dev/variables.tf 에서 각각 사용할 변수에 값을 넣어줍니다.

Module main 생성

env/dev/main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

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

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

  # prj
  project_name = var.project_name
  environment = var.environment
  key_name = var.key_name

  # VPC
  cidr_vpc = var.cidr_vpc
  cidr_public1 = var.cidr_public1
  cidr_public2 = var.cidr_public2
  cidr_public3 = var.cidr_public3
  cidr_public4 = var.cidr_public4
  cidr_private1 = var.cidr_private1
  cidr_private2 = var.cidr_private2
  cidr_private3 = var.cidr_private3
  cidr_private4 = var.cidr_private4

  # Public EC2
  bastion_ami           = var.bastion_ami
  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_ami           = var.Private_EC2_ami
  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
}

마지막으로 main에서는 모듈을 관리합니다.

구축한 환경 테스트

콘솔로 들어와서 확인해 보면 문제 없이 2대의 EC2 인스턴스가 생성된 것을 확인할 수 있습니다.

Private EC2 인스턴스인  i-095c555037291560b 인스턴스에서 SSM 접속을 시도해 보면 IAM Role이 정상 할당 되어 있고, NAT Gateway가 생성되어 있는 상태이기 때문에 문제 없이 SSM 접속이 가능한 것을 확인할 수 있습니다.

본 블로그 게시글을 보시고 문의 사항이 있으신 분들은 클래스메소드코리아 (info@classmethod.kr)로 연락 주시면 빠른 시일 내 담당자가 회신 드릴 수 있도록 하겠습니다 !