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

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

이번에는 Terraform으로 EC2 Instance 생성해 보는 방법 대해서 정리해 봤습니다.
Clock Icon2022.06.16 23:59

この記事は公開されてから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 접속이 가능한 것을 확인할 수 있습니다.

본 블로그 게시글을 읽고 궁금한 사항이 있으신 분들은 kis2702@naver.com로 보내주시면 감사하겠습니다.

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.