테라폼에서 Provisioner가 어떤것이고 EC2 Userdata는 무엇이며 둘 간의 차이는 무엇인지 알아보자.
Provisioner
테라폼에서 공식적으로 지원하는 문법이다. 세 가지 종류가 있는데 다음과 같다.
- file: 로컬 -> 리모트 파일복사
- local-exec: 로컬PC에서 명령어 수행
- remote-exec: 리모트 머신에서 명령어 수행 (SSH, WinRM 과 같은 프로토콜을 지원)
그래서 위 세가지 경우를 처리하기 위해 테라폼에서 provisioner라는 녀석을 제공해준다고 생각하면 된다.
이 녀석은 기본적으로는 첫 리소스 생성 시점에 수행되지만 여러 옵션들을 통해 삭제 시점이라던가 매번 수행할 수 있도록 커스터마이징 할 수도 있다.
EC2 Userdata
우선 Userdata는 무엇이냐? AWS EC2 인스턴스를 만들 때 하단에 Advanced Settings 설정을 열어보면 Userdata가 보일것이다.
이 작업은 부팅 시점에 부트스트래핑을 해주는 녀석이다. 예를 들면, 사용자를 생성한다거나, 파일을 구성한다거나, 소프트웨어 설치를 한다거나 하는 작업들을 첫 부팅 시점에만 실행을 해준다.
실습
그럼 이제 provisioner를 이용해서 EC2를 만들어보는것과 Userdata를 이용해서 EC2를 만들어보고 어떤 차이점이 있고 어떻게 돌아가는지 직접 확인해보자.
다음이 이 내용을 실습하기 위한 파일 구조이다.
하나씩 살펴보자.
versions.tf
이 파일은 기본적으로 요구되는 버전을 명시한 파일이다. 특이사항은 없다.
terraform {
required_version = "~> 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
outputs.tf
provisioner와 userdata를 각각 이용해서 만든 EC2 인스턴스에 대한 Public/Private IP와 DNS관련 데이터이다. 특이사항은 없다.
output "provisioner_instance" {
value = {
public_ip = aws_instance.provisioner.public_ip
public_dns = aws_instance.provisioner.public_dns
private_ip = aws_instance.provisioner.private_ip
private_dns = aws_instance.provisioner.private_dns
}
}
output "userdata_instance" {
value = {
public_ip = aws_instance.userdata.public_ip
public_dns = aws_instance.userdata.public_dns
private_ip = aws_instance.userdata.private_ip
private_dns = aws_instance.userdata.private_dns
}
}
files/index.html
이 파일은 nginx로 띄워줄 index.html 파일이다. 특이사항은 없다.
<h1>Hello Chyonee!</h1>
files/install-nginx.sh
이 파일은 nginx 설치 관련 ShellScript 파일이다. 역시 특이사항은 없다.
#!/bin/bash
sudo apt-get update
sudo apt-get install -y nginx
main.tf
provider "aws" {
region = "ap-northeast-2"
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"]
}
locals {
vpc_name = "cwchoiit-vpc"
common_tags = {
"Project" = "provisioner-userdata"
}
}
module "security_group" {
source = "tedilabs/network/aws//modules/security-group"
version = "0.24.0"
name = "${local.vpc_name}-provisioner-userdata"
description = "Security Group for SSH."
vpc_id = "vpc-04a888e12b489eb12"
ingress_rules = [
{
id = "ssh"
protocol = "tcp"
from_port = 22
to_port = 22
cidr_blocks = ["0.0.0.0/0"]
description = "Allow SSH from anywhere."
},
{
id = "http"
protocol = "tcp"
from_port = 80
to_port = 80
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTP from anywhere."
}
]
egress_rules = [
{
id = "all/all"
description = "Allow to communicate to the Internet."
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
]
tags = local.common_tags
}
resource "aws_instance" "userdata" {
ami = data.aws_ami.ubuntu.image_id
instance_type = "t2.micro"
key_name = "cwchoiit-ec2-keypair-home"
subnet_id = "subnet-0b23fd05b5919269e"
associate_public_ip_address = true
user_data = <<EOT
#!/bin/bash
sudo apt-get update
sudo apt-get install -y nginx
EOT
vpc_security_group_ids = [
module.security_group.id,
]
tags = {
Name = "cwchoiit-userdata"
}
}
resource "aws_instance" "provisioner" {
ami = data.aws_ami.ubuntu.image_id
instance_type = "t2.micro"
key_name = "cwchoiit-ec2-keypair-home"
subnet_id = "subnet-0b23fd05b5919269e"
associate_public_ip_address = true
vpc_security_group_ids = [
module.security_group.id,
]
tags = {
Name = "cwchoiit-provisioner"
}
}
resource "null_resource" "provisioner" {
triggers = {
instance_id = aws_instance.provisioner.id
script = filemd5("${path.module}/files/install-nginx.sh")
index_file = filemd5("${path.module}/files/index.html")
}
provisioner "local-exec" {
command = "echo Hello World"
}
provisioner "file" {
source = "${path.module}/files/index.html"
destination = "/tmp/index.html"
connection {
type = "ssh"
user = "ubuntu"
host = aws_instance.provisioner.public_ip
private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
}
}
provisioner "remote-exec" {
script = "${path.module}/files/install-nginx.sh"
connection {
type = "ssh"
user = "ubuntu"
host = aws_instance.provisioner.public_ip
private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
}
}
provisioner "remote-exec" {
inline = [
"sudo cp /tmp/index.html /var/www/html/index.html"
]
connection {
type = "ssh"
user = "ubuntu"
host = aws_instance.provisioner.public_ip
private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
}
}
}
하나씩 뜯어보자. 위 부분(AWS Provider, AMI 가져오는 Data Source, Local Variables은 생략하겠다. 너무 많이 봐왔던 것들이기 때문)
EC2에 할당할 Security Group은 누군가가 만들어놓은 Module을 가져다가 사용했다. Inbound/Outbound 규칙은 ingress, egress에 매핑된 상태고, name, description, vpc_id를 할당해주었다.
module "security_group" {
source = "tedilabs/network/aws//modules/security-group"
version = "0.24.0"
name = "${local.vpc_name}-provisioner-userdata"
description = "Security Group for SSH."
vpc_id = "vpc-04a888e12b489eb12"
ingress_rules = [
{
id = "ssh"
protocol = "tcp"
from_port = 22
to_port = 22
cidr_blocks = ["0.0.0.0/0"]
description = "Allow SSH from anywhere."
},
{
id = "http"
protocol = "tcp"
from_port = 80
to_port = 80
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTP from anywhere."
}
]
egress_rules = [
{
id = "all/all"
description = "Allow to communicate to the Internet."
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
]
tags = local.common_tags
}
이제 Userdata를 이용해서 AWS EC2 인스턴스를 만드는 부분이다. 기본적으로 ami, instance_type, subnet_id, associate_public_ip_address는 할당해주었다. 여기서 새롭게 보이는 key_name은 EC2에 SSH로 접속할 때 사용했던 Key pair의 Name이다. 그리고 이제 핵심 부분인 user_data가 있다. 보면 EOT로 시작해서 EOT로 끝나는데 그냥 Multiple 문자열을 받기 위해 저렇게 사용한다. 그리고 첫 부팅시에만 적용될 sudo apt-get update와 nginx를 설치하는 작업을 한다. 그 다음 위에서 설명한 security_group을 지정했다.
resource "aws_instance" "userdata" {
ami = data.aws_ami.ubuntu.image_id
instance_type = "t2.micro"
key_name = "cwchoiit-ec2-keypair-home"
subnet_id = "subnet-0b23fd05b5919269e"
associate_public_ip_address = true
user_data = <<EOT
#!/bin/bash
sudo apt-get update
sudo apt-get install -y nginx
EOT
vpc_security_group_ids = [
module.security_group.id,
]
tags = {
Name = "cwchoiit-userdata"
}
}
그 다음 EC2 하나를 더 만든다. 이 부분은 provisioner를 사용하는 부분은 없다. 그 바로 밑에 있는데 우선 리소스 이름 자체는 provisioner로 설정했다. 위 내용과 설명은 동일하다.
resource "aws_instance" "provisioner" {
ami = data.aws_ami.ubuntu.image_id
instance_type = "t2.micro"
key_name = "cwchoiit-ec2-keypair-home"
subnet_id = "subnet-0b23fd05b5919269e"
associate_public_ip_address = true
vpc_security_group_ids = [
module.security_group.id,
]
tags = {
Name = "cwchoiit-provisioner"
}
}
이제 이 부분이 핵심이다. null_resource는 이 역시 Terraform에서 지원하는 Provider 중 하나인데 이 녀석은 딱 한가지 Attribute를 가지고 있다. triggers 라는 Attribute인데 이 triggers는 그 안에 지정한 것들 중 하나라도 변경 사항이 생기면 이 null_resource라는 전체 리소스를 재실행한다. 즉 리소스를 다시 만들어낸다. 그리고 이것을 사용해서 실제 provisioner를 사용했다.
resource "null_resource" "provisioner" {
triggers = {
instance_id = aws_instance.provisioner.id
script = filemd5("${path.module}/files/install-nginx.sh")
index_file = filemd5("${path.module}/files/index.html")
}
provisioner "local-exec" {
command = "echo Hello World"
}
provisioner "file" {
source = "${path.module}/files/index.html"
destination = "/tmp/index.html"
connection {
type = "ssh"
user = "ubuntu"
host = aws_instance.provisioner.public_ip
private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
}
}
provisioner "remote-exec" {
script = "${path.module}/files/install-nginx.sh"
connection {
type = "ssh"
user = "ubuntu"
host = aws_instance.provisioner.public_ip
private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
}
}
provisioner "remote-exec" {
inline = [
"sudo cp /tmp/index.html /var/www/html/index.html"
]
connection {
type = "ssh"
user = "ubuntu"
host = aws_instance.provisioner.public_ip
private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
}
}
}
triggers에 3개가 있다. instance_id, script, index_file. 우선 instance_id는 위에서 만든 provisioner라는 이름의 리소스를 가진 리소스의 ID가 변경되거나, script, index_file은 files 폴더에 있는 파일의 해시값이 변경된다(파일 내용이 변경됨)면 이 리소스가 재실행된다는 의미이다.
여기에 local-exec는 로컬 PC에서 명령어를 수행하는 부분이다. 의미없지만 잘 수행되는지 Hello World를 echo로 실행한다.
file은 로컬에서 리모트로 파일을 전송한다. 그래서 source랑 destination 속성이 있다. 저 부분은 뭐가 뭔지 한 눈에 알 수 있을것이고 connection 부분이 중요하다. 이 부분이 어떻게 전송할지에 대한 내용이기 때문이다. 우선 SSH로 보내고 유저 이름은 "ubuntu"이다. Host는 위에서 provisioner라는 리소스 이름의 인스턴스 Public IP가 된다. 그리고 private_key가 이제 EC2에 SSH로 접속할 때 사용하는 .pem 파일이다. 이 .pem 파일은 AWS Console에서 EC2 인스턴스 만들 때 Key pair로 사용하는 그 .pem 파일이다.
그리고 remote-exec는 리모트 머신에서 명령어를 수행한다. 그 수행하는 명령어가 바로 script = "실행할 스크립트 파일의 경로" 또는 inline = ["명령어", "명령어"]이다. 이 명령어를 작성하는 방법은 세가지가 있다. script, scripts, inline. script와 inline은 보았고, scripts는 스크립트 파일 한개가 아니라 여러개를 실행할 때 사용한다.
그래서 이 main.tf에 대해 Apply를 하면 기존에 있는 VPC를 사용해서(저 위에 vpc-id가 기존에 있는 내 VPC ID를 붙인 것) 해당 VPC의 Subnet역시 기존에 있는 Subnet에 EC2 두개가 만들어진다. (Userdata로, Provisioner로) 그리고 각 EC2는 Nginx를 설치하는데 Userdata는 설치하는 것까지가 끝이다. 그리고 그것은 딱 한번 최초 부팅시에만 실행될 것이다. 그게 Userdata의 특징이니까. 그러나 provisioner로 만든 EC2는 null_resource를 사용해서 감시하는 3개의 트리거가 발동될 때마다 새롭게 null_resource에 작성한 작업들을 수행하게 된다. 왜냐하면 null_resource에서 작업하기로 하는 HOST가 전부 provisioner로 만든 EC2의 Public IP를 가르키기 때문.
그래서 이 것을 Apply해보자. 다음과 같이 정상적으로 Apply가 됐고 EC2 인스턴스 두개가 생겼다.
AWS Console에 들어가보면 만든 EC2 두개가 보인다.
그리고 각 EC2의 Security도 잘 적용된 상태다.
이 상태에서 저 Outputs에서 알려준 Public IP를 접근해보자. Nginx를 두 EC2 모두 설치했기 때문에 80으로 접속 시 Nginx 화면이 보여야한다.
Userdata로 만든 EC2
Provisioner로 만든 EC2
Provisioner로 만든 EC2는 왜 다를까? null_resource에서 index.html 파일을 Nginx가 기본으로 뿌려주는 경로에 위치시켰기 때문에 내가 만든 이 index.html 파일이 보이는 것이다.
그럼 여기서 한 발 더 나아가서 이 파일(index.html)이 수정되면? => 트리거가 발동 => provisioner로 만든 EC2가 Replace 될 것
files/index.html
다음과 같이 수정해보자.
<h1>Hello Chyoni!</h1>
그 후 Apply 진행. 변경된다는 Plan이 보인다.
작업이 진행되고 다시 provisioner로 띄운 EC2로 들어가보면 다음과 같이 변경 사항이 적용됐음을 확인 가능하다.
'IaC(Infrastructure as Code)' 카테고리의 다른 글
Packer Part. 2 (Builder) (0) | 2024.03.14 |
---|---|
Packer Part. 1 (0) | 2024.03.14 |
terraform_remote_state (0) | 2024.03.10 |
Terraform 나만의 Module 만들어보기 (0) | 2024.03.10 |
Terraform Cloud (0) | 2024.03.10 |