테라폼(Terraform)으로 EC2 Instance 생성해 보기
안녕하세요 클래스메소드 김재욱(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 접속이 가능한 것을 확인할 수 있습니다.
본 블로그 게시글을 읽고 궁금한 사항이 있으신 분들은 [email protected]로 보내주시면 감사하겠습니다.