본문 바로가기
T101 4기

2-2. EC2 생성시 PEM Key 중복 체크 및 생성 코드

by yeongki0944 2024. 6. 22.
해당 포스팅은 테라폼 문법을 학습하며, 테스트 용으로 만든 포스팅입니다.
실제 인프라 운영에 적용하기에 부적절합니다.

해당 소스의 목적

  • EC2 인스턴스를 생성하기 위해 필요한 PEM 키가 존재하는지 확인하고, 존재하지 않는 경우 새로운 키를 생성하여 사용하는 코드

 

파일 구조

.
├── main.tf
├── provider.tf
├── variables.tf
├── locals.tf
├── key_management.tf
└── outputs.tf

 

주요 개념

- Null Resource

[null_resource 개념]

https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource

 

  • null_resource는 Terraform에서 실제 인프라 자원을 생성하지 하지 않음. 주로 프로비저닝할 필요가 없는 작업에 사용.
  • null_resource의 기본 동작은 terraform apply 명령어를 처음 실행할 때 한 번만 실행
  • trigger를 통해 특정 조건에서 중복 실행 가능.
  • 주로 사용되는 케이스
    • 스크립트 실행: local-exec나 remote-exec 프로비저너를 사용하여 로컬이나 원격 서버에서 스크립트를 실행 가능.
    • 의존성 설정: 특정 리소스가 생성된 후 추가 작업을 수행할 때 사용됨.
    • 조건부 논리: 리소스가 변경되었을 때 특정 작업을 수행하도록 설정 가능.

[null_resource 선언 구문]

resource "null_resource" "example" {
provisioner "local-exec" {
command = "echo This command will execute whenever the configuration changes"
}
}

 

[null_resource trigger]

# trigger가 없는 null_resource
# terraform apply 명령어를 처음 실행할 때만 딱 한번 실행
resource "null_resource" "example" {
provisioner "local-exec" {
command = "echo This command will execute only once during apply"
}
}
# trigger가 있는 null_resource
resource "null_resource" "example" {
# 트리거 > 현재 타임스탬프 기준
# 트리거 값이 변경되면, 실행 > 타임스탬프는 매번 변경되기에 apply시 항상 실행
triggers = {
always_run = timestamp()
}
provisioner "local-exec" {
command = "echo This specific command will execute every time during apply as triggers are used"
}
}

 

- provisoner

[provisoner 개념]

https://developer.hashicorp.com/terraform/language/resources/provisioners/syntax

  • Provisioner는 Terraform에서 리소스를 생성한 후 특정 작업을 수행하기 위해 사용함.
  • 프로비저너는 리소스 생성 후 스크립트 실행, 파일 복사, 명령 실행 등의 작업을 자동화하는 데 유용함.

 

[provisoner 주요 유형]

 

  • local-exec
    • 로컬 시스템에서 명령을 실행합니다.
  • remote-exec
    • 원격 시스템에서 명령을 실행합니다.

 

 

[provisoner 선언 구문]

resource "null_resource" "example" {
provisioner "local-exec" {
command = "echo This command will execute"
}
}

 

 

- local-exec

[local-exec 개념]

  • local-exec은 Terraform에서 로컬 머신에서 명령을 실행하기 위한 프로비저너
  • local-exec 프로비저너를 사용하면, Terraform이 실행되는 머신에서 직접 스크립트나 명령을 실행할 수 있음.

[local-exec 사용 예제]

resource "null_resource" "example" {
provisioner "local-exec" {
command = "echo This command will execute on the local machine"
}
}

 

소스코드

terraform graph ❘ dot -Tpng > graph.png

- main.tf

provider "aws" {
region = var.region
}
# EC2 인스턴스를 생성
resource "aws_instance" "ec2_instance" {
count = var.instance_count
ami = "ami-0c55b159cbfafe1f0" # 원하는 AMI ID로 변경하세요
instance_type = "t2.micro"
key_name = var.pem_file_name
tags = {
Name = "${var.ec2_instance_name}-${count.index}"
}
depends_on = [null_resource.create_new_key]
}
  • EC2 인스턴스를 생성하는 코드
  • count 매개변수를 사용하여 여러 개의 인스턴스를 생성 가능
  • depends_on 을 사용하여 AWS상에 Key가 존재하는지 체크 후 EC2 인스턴스 생성

 

- key_management.tf

# PEM 폴더를 생성하는 null_resource (폴더가 없는 경우)
resource "null_resource" "create_pem_folder" {
provisioner "local-exec" {
command = "mkdir -p ${path.module}/pem"
}
triggers = {
pem_folder = "${timestamp()}"
}
}
# 키 페어가 존재하지 않으면 새 키를 생성하는 null_resource
resource "null_resource" "create_new_key" {
provisioner "local-exec" {
command = <<EOT
if ! aws ec2 describe-key-pairs --key-names ${var.pem_file_name} 2>/dev/null; then
aws ec2 create-key-pair --key-name ${var.pem_file_name} --query 'KeyMaterial' --output text > ${local.new_key_file_path}
chmod 400 ${local.new_key_file_path}
fi
EOT
}
triggers = {
create_key = "${var.pem_file_name}-${timestamp()}"
}
depends_on = [null_resource.create_pem_folder]
}
  • PEM를 저장할 폴더를 생성
  • aws ec2 describe-key-pairs 명령어를 통해, 키 페어가 존재하는지 안하는지 체크
  • 키 페어가 존재하지 않으면, aws ec2 create-key-pair 명령어로 키 페어 생성 후, 해당 키 페어를 PEM 폴더에 저장
  • triggers에 timestamp를 적용해 항상 실행하여 체크하도록 설정

 

 

- variables.tf

variable "pem_file_name" {
description = "The name of the PEM key file (without .pem extension)"
type = string
default = "us-east-2-key-yg"
}
variable "ec2_instance_name" {
description = "The name of the EC2 instance"
type = string
default = "my-ec2-instance-edit"
}
variable "region" {
description = "The AWS region to create resources in"
type = string
default = "us-east-2"
}
variable "instance_count" {
description = "The number of EC2 instances to create"
type = number
default = 3
}
  • pem_file_name : 키 페어 이름, 확장자명을 생략하고 입력
  • instance_count : count를 사용해서 EC2 리소스 반복 생성

 

- locals.tf

locals {
new_key_file_path = "${path.module}/pem/${var.pem_file_name}.pem"
}
  • 새로운 키페어가 생성시, 해당 키페어의 상대경로를 output으로 출력하기 위한 locals 변수
  • variables를 사용할 경우 에러가 발생
    • variables는 외부로부터 입력을 받기 위해 사용
    • locals는 내부 계산 및 중간 값을 저장하기 위해 사용

 

- output.tf

# 새로운 키 파일 경로를 출력하는 output (AWS에서 PEM 키 파일이 없는 경우)
output "new_key_file_path" {
value = local.new_key_file_path
}

 

 

코드 실행 결과

- 키 페어가 존재하는 경우

키 페어 존재
pem_file_name을 미리 생성된 pem_key이름과 동일하게 설정
terraform init

 

실행 전, 후

admin:~/environment/t101/week2/conditional-resource-ec2-pemkey_v2 $ terraform apply -auto-approve
\
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:
# aws_instance.ec2_instance[0] will be created
+ resource "aws_instance" "ec2_instance" {
+ ami = "ami-0c55b159cbfafe1f0"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ disable_api_stop = (known after apply)
+ disable_api_termination = (known after apply)
+ ebs_optimized = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ host_resource_group_arn = (known after apply)
+ iam_instance_profile = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (known after apply)
+ instance_lifecycle = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t2.micro"
+ ipv6_address_count = (known after apply)
+ ipv6_addresses = (known after apply)
+ key_name = "us-east-2-key-yg"
+ monitoring = (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ placement_partition_number = (known after apply)
+ primary_network_interface_id = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ spot_instance_request_id = (known after apply)
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "my-ec2-instance-edit-0"
}
+ tags_all = {
+ "Name" = "my-ec2-instance-edit-0"
}
+ tenancy = (known after apply)
+ user_data = (known after apply)
+ user_data_base64 = (known after apply)
+ user_data_replace_on_change = false
+ vpc_security_group_ids = (known after apply)
}
# aws_instance.ec2_instance[1] will be created
+ resource "aws_instance" "ec2_instance" {
+ ami = "ami-0c55b159cbfafe1f0"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ disable_api_stop = (known after apply)
+ disable_api_termination = (known after apply)
+ ebs_optimized = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ host_resource_group_arn = (known after apply)
+ iam_instance_profile = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (known after apply)
+ instance_lifecycle = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t2.micro"
+ ipv6_address_count = (known after apply)
+ ipv6_addresses = (known after apply)
+ key_name = "us-east-2-key-yg"
+ monitoring = (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ placement_partition_number = (known after apply)
+ primary_network_interface_id = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ spot_instance_request_id = (known after apply)
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "my-ec2-instance-edit-1"
}
+ tags_all = {
+ "Name" = "my-ec2-instance-edit-1"
}
+ tenancy = (known after apply)
+ user_data = (known after apply)
+ user_data_base64 = (known after apply)
+ user_data_replace_on_change = false
+ vpc_security_group_ids = (known after apply)
}
# aws_instance.ec2_instance[2] will be created
+ resource "aws_instance" "ec2_instance" {
+ ami = "ami-0c55b159cbfafe1f0"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ disable_api_stop = (known after apply)
+ disable_api_termination = (known after apply)
+ ebs_optimized = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ host_resource_group_arn = (known after apply)
+ iam_instance_profile = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (known after apply)
+ instance_lifecycle = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t2.micro"
+ ipv6_address_count = (known after apply)
+ ipv6_addresses = (known after apply)
+ key_name = "us-east-2-key-yg"
+ monitoring = (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ placement_partition_number = (known after apply)
+ primary_network_interface_id = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ spot_instance_request_id = (known after apply)
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "my-ec2-instance-edit-2"
}
+ tags_all = {
+ "Name" = "my-ec2-instance-edit-2"
}
+ tenancy = (known after apply)
+ user_data = (known after apply)
+ user_data_base64 = (known after apply)
+ user_data_replace_on_change = false
+ vpc_security_group_ids = (known after apply)
}
# null_resource.create_new_key will be created
+ resource "null_resource" "create_new_key" {
+ id = (known after apply)
+ triggers = {
+ "create_key" = (known after apply)
}
}
# null_resource.create_pem_folder will be created
+ resource "null_resource" "create_pem_folder" {
+ id = (known after apply)
+ triggers = {
+ "pem_folder" = (known after apply)
}
}
Plan: 5 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ new_key_file_path = "./pem/us-east-2-key-yg.pem"
null_resource.create_pem_folder: Creating...
null_resource.create_pem_folder: Provisioning with 'local-exec'...
null_resource.create_pem_folder (local-exec): Executing: ["/bin/sh" "-c" "mkdir -p ./pem"]
null_resource.create_pem_folder: Creation complete after 0s [id=7542075506616754046]
null_resource.create_new_key: Creating...
null_resource.create_new_key: Provisioning with 'local-exec'...
null_resource.create_new_key (local-exec): Executing: ["/bin/sh" "-c" " if ! aws ec2 describe-key-pairs --key-names us-east-2-key-yg 2>/dev/null; then\n aws ec2 create-key-pair --key-name us-east-2-key-yg --query 'KeyMaterial' --output text > ./pem/us-east-2-key-yg.pem\n chmod 400 ./pem/us-east-2-key-yg.pem\n fi\n"]
null_resource.create_new_key (local-exec): {
null_resource.create_new_key (local-exec): "KeyPairs": [
null_resource.create_new_key (local-exec): {
null_resource.create_new_key (local-exec): "KeyPairId": "key-0889a20a1a1099c0e",
null_resource.create_new_key (local-exec): "KeyFingerprint": "3a:ee:93:f8:21:b3:9d:b3:ae:1f:87:f7:1c:50:28:4d:46:33:af:0b",
null_resource.create_new_key (local-exec): "KeyName": "us-east-2-key-yg",
null_resource.create_new_key (local-exec): "KeyType": "rsa",
null_resource.create_new_key (local-exec): "Tags": [],
null_resource.create_new_key (local-exec): "CreateTime": "2024-06-22T14:43:21.803000+00:00"
null_resource.create_new_key (local-exec): }
null_resource.create_new_key (local-exec): ]
null_resource.create_new_key (local-exec): }
null_resource.create_new_key: Creation complete after 1s [id=3573047788361369149]
aws_instance.ec2_instance[2]: Creating...
aws_instance.ec2_instance[0]: Creating...
aws_instance.ec2_instance[1]: Creating...
aws_instance.ec2_instance[2]: Still creating... [10s elapsed]
aws_instance.ec2_instance[0]: Still creating... [10s elapsed]
aws_instance.ec2_instance[1]: Still creating... [10s elapsed]
aws_instance.ec2_instance[2]: Still creating... [20s elapsed]
aws_instance.ec2_instance[0]: Still creating... [20s elapsed]
aws_instance.ec2_instance[1]: Still creating... [20s elapsed]
aws_instance.ec2_instance[1]: Creation complete after 21s [id=i-06894b1e9d8c3cf63]
aws_instance.ec2_instance[2]: Creation complete after 21s [id=i-09a4eb1e875e86470]
aws_instance.ec2_instance[0]: Still creating... [30s elapsed]
aws_instance.ec2_instance[0]: Creation complete after 31s [id=i-09797f1ff30513c91]
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
Outputs:
new_key_file_path = "./pem/us-east-2-key-yg.pem"

 

첫번째 apply 실행 후 코드 변경없이 두번째 apply > trigger중에 timestamp가 들어간 부분만 다시 실행됨.
ec2 생성 완료
ec2 key파일 체크

 

- EC2 Tag이름 변경

변경 전

 

 

tf 코드 수정
tf plan
변경 완료

 

 

- 기존 EC2에 새로운 Key pair을 생성해서 적용할 경우

 

key pair이름 변경
plan 일부

 

ec2 삭제

 

pem_key 생성 과정

 

EC2 생성 및 outputs