diff --git a/README.md b/README.md index 1259abe..4c99c5d 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,47 @@ terraform apply terraform destroy ``` +## EC2 SSH 접속 방법 + +### SSH 키 생성 및 저장 + +**로컬 환경에서 SSH 키를 생성하고 저장하려면:** +```bash +# SSH 키를 로컬에 저장하면서 인프라 생성 +terraform apply -var="save_private_key_locally=true" +``` + +### SSH 접속 + +**생성된 키로 EC2 인스턴스에 접속:** +```bash +# API 서버 접속 +ssh -i ./generated/annyang-key.pem ec2-user@ + +# AI 서버 접속 (AI 모듈이 활성화된 경우) +ssh -i ./generated/annyang-ai-key.pem ec2-user@ +``` + +### Terraform 출력에서 IP 주소 확인 + +**EC2 인스턴스의 퍼블릭 IP 주소 확인:** +```bash +# API 서버 IP 확인 +terraform output ec2_public_ip + +# AI 서버 IP 확인 (AI 모듈이 활성화된 경우) +terraform output ec2_ai_public_ip + +# 모든 출력 확인 +terraform output +``` + +### 주의사항 + +- **GitHub Actions 실행 시**: `save_private_key_locally=false`가 기본값이므로 로컬 키 파일이 생성되지 않습니다. +- **보안**: 생성된 SSH 키 파일(`generated/` 디렉토리)은 `.gitignore`에 포함되어 Git에 커밋되지 않습니다. +- **권한**: SSH 키 파일은 자동으로 적절한 권한(600)으로 설정됩니다. + ## CI/CD 배포 통합 방법 ### 애플리케이션 프로젝트 준비 @@ -118,4 +159,5 @@ deploy: ## 참고사항 - RDS 비밀번호는 AWS SSM Parameter Store에 저장되며, 초기 비밀번호는 배포 후 변경해야 합니다. - EC2 SSH 키는 `generated/` 디렉토리에 저장됩니다. -- 애플리케이션이 SSM에서 환경 변수를 가져오기 위해서는 EC2 인스턴스에 적절한 IAM 권한이 필요합니다 (이미 구성됨). \ No newline at end of file +- SSH 키 파일은 보안을 위해 Git에 커밋되지 않습니다. +- 애플리케이션이 SSM에서 환경 변수를 가져오기 위해서는 EC2 인스턴스에 적절한 IAM 권한이 필요합니다 (이미 구성됨). \ No newline at end of file diff --git a/main.tf b/main.tf index ef1819e..20d9e1d 100644 --- a/main.tf +++ b/main.tf @@ -94,6 +94,7 @@ module "ec2" { # 보안 그룹 모듈에서 생성된 ID 사용 security_group_id = module.sg.ec2_security_group_id + save_private_key_locally = false } module "acm" { @@ -139,6 +140,7 @@ module "ec2-ai" { # AI 서버 보안 그룹 사용 security_group_id = module.sg.ai_server_security_group_id + save_private_key_locally = false } # Terraform 백엔드용 S3 버킷 모듈 (일회성 생성용) diff --git a/modules/ec2-ai/main.tf b/modules/ec2-ai/main.tf index b457e58..004b706 100644 --- a/modules/ec2-ai/main.tf +++ b/modules/ec2-ai/main.tf @@ -1,48 +1,33 @@ # SSH 키 페어 생성 locals { key_name = "${var.project_name}-ai-key" - key_file_path = "${path.cwd}/generated/${local.key_name}" - key_dir = "${path.cwd}/generated" - should_create_key = !fileexists(local.key_file_path) -} - -# generated 디렉토리 생성 -resource "local_file" "ensure_directory" { - count = local.should_create_key ? 1 : 0 - filename = "${local.key_dir}/.keep" - content = "" - - provisioner "local-exec" { - command = "mkdir -p ${local.key_dir}" - } } +# TLS 프라이빗 키 생성 resource "tls_private_key" "ssh" { - count = local.should_create_key ? 1 : 0 algorithm = "RSA" rsa_bits = 4096 - - depends_on = [local_file.ensure_directory] } -# 로컬에 PEM 파일 저장 -resource "local_file" "private_key" { - count = local.should_create_key ? 1 : 0 - content = local.should_create_key ? tls_private_key.ssh[0].private_key_pem : "" - filename = local.key_file_path - - # 파일 권한 설정 (0600: 소유자만 읽기/쓰기 가능) - file_permission = "0600" - - depends_on = [tls_private_key.ssh] -} - -# AWS 키 페어 생성 +# AWS 키 페어 생성 (퍼블릭 키는 tls_private_key 리소스에서 직접 가져옴) resource "aws_key_pair" "key_pair" { key_name = local.key_name - public_key = file("${local.key_file_path}.pub") + public_key = tls_private_key.ssh.public_key_openssh +} - depends_on = [local_file.private_key] +# 로컬 환경에서만 PEM 파일 저장 (GitHub Actions에서는 실행되지 않음) +resource "local_file" "private_key" { + count = var.save_private_key_locally ? 1 : 0 + + content = tls_private_key.ssh.private_key_pem + filename = "${path.cwd}/generated/${local.key_name}.pem" + + file_permission = "0600" + + # 디렉토리 생성 + provisioner "local-exec" { + command = "mkdir -p ${path.cwd}/generated" + } } # 사용자 데이터 스크립트 - Docker 설치 및 ECR에서 Flask 이미지 가져오기 @@ -111,4 +96,41 @@ resource "aws_instance" "ai_server" { Application = var.project_name ManagedBy = "terraform" } +} + +# SSH 키를 SSM Parameter Store에 저장 (안전한 접근을 위해) +resource "aws_ssm_parameter" "ai_server_private_key" { + name = "/${var.project_name}/ec2-ai/ssh/private-key" + type = "SecureString" + value = tls_private_key.ssh.private_key_pem + + description = "AI 서버 SSH 프라이빗 키" + + tags = { + Name = "${var.project_name}-ai-server-ssh-key" + Application = var.project_name + ManagedBy = "terraform" + } +} + +# AI 서버 접속 정보도 함께 저장 +resource "aws_ssm_parameter" "ai_server_connection_info" { + name = "/${var.project_name}/ec2-ai/connection/info" + type = "String" + value = jsonencode({ + public_ip = aws_instance.ai_server.public_ip + private_ip = aws_instance.ai_server.private_ip + instance_id = aws_instance.ai_server.id + key_name = aws_key_pair.key_pair.key_name + user = "ec2-user" + ssh_command = "ssh -i ec2-user@${aws_instance.ai_server.public_ip}" + }) + + description = "AI 서버 접속 정보" + + tags = { + Name = "${var.project_name}-ai-server-connection-info" + Application = var.project_name + ManagedBy = "terraform" + } } \ No newline at end of file diff --git a/modules/ec2-ai/outputs.tf b/modules/ec2-ai/outputs.tf index 011ac52..223828d 100644 --- a/modules/ec2-ai/outputs.tf +++ b/modules/ec2-ai/outputs.tf @@ -3,17 +3,28 @@ output "instance_id" { value = aws_instance.ai_server.id } +output "public_ip" { + description = "AI 서버 EC2 인스턴스의 퍼블릭 IP" + value = aws_instance.ai_server.public_ip +} + output "private_ip" { description = "AI 서버 EC2 인스턴스의 프라이빗 IP" value = aws_instance.ai_server.private_ip } output "ai_server_url" { - description = "AI 서버 URL (퍼블릭 IP)" + description = "AI 서버 URL (API 서버 내부 통신용)" value = "http://${aws_instance.ai_server.private_ip}:${var.port}" } -output "ssh_command" { - description = "SSH 접속 명령어 (API 서버를 통해 접속해야 함)" - value = "ssh -i ${local.key_file_path} ec2-user@${aws_instance.ai_server.public_ip}" +output "key_pair_name" { + description = "AI 서버 SSH 키 페어 이름" + value = aws_key_pair.key_pair.key_name +} + +output "private_key_pem" { + description = "AI 서버 프라이빗 키 (PEM 형식)" + value = tls_private_key.ssh.private_key_pem + sensitive = true } diff --git a/modules/ec2-ai/variables.tf b/modules/ec2-ai/variables.tf index b8994fe..76021dc 100644 --- a/modules/ec2-ai/variables.tf +++ b/modules/ec2-ai/variables.tf @@ -35,4 +35,10 @@ variable "port" { description = "AI 서버가 실행될 포트" type = number default = 5000 # AI 서버의 기본 포트 -} \ No newline at end of file +} + +variable "save_private_key_locally" { + description = "프라이빗 키를 로컬에 저장할지 여부 (GitHub Actions에서는 false)" + type = bool + default = false +} diff --git a/modules/ec2/main.tf b/modules/ec2/main.tf index bdcb234..1eb55e6 100644 --- a/modules/ec2/main.tf +++ b/modules/ec2/main.tf @@ -1,48 +1,33 @@ # SSH 키 페어 생성 locals { key_name = "${var.project_name}-key" - key_file_path = "${path.cwd}/generated/${local.key_name}" - key_dir = "${path.cwd}/generated" - should_create_key = !fileexists(local.key_file_path) -} - -# generated 디렉토리 생성 -resource "local_file" "ensure_directory" { - count = local.should_create_key ? 1 : 0 - filename = "${local.key_dir}/.keep" - content = "" - - provisioner "local-exec" { - command = "mkdir -p ${local.key_dir}" - } } +# TLS 프라이빗 키 생성 resource "tls_private_key" "ssh" { - count = local.should_create_key ? 1 : 0 algorithm = "RSA" rsa_bits = 4096 - - depends_on = [local_file.ensure_directory] } -# 로컬에 PEM 파일 저장 -resource "local_file" "private_key" { - count = local.should_create_key ? 1 : 0 - content = local.should_create_key ? tls_private_key.ssh[0].private_key_pem : "" - filename = local.key_file_path - - # 파일 권한 설정 (0600: 소유자만 읽기/쓰기 가능) - file_permission = "0600" - - depends_on = [tls_private_key.ssh] -} - -# AWS 키 페어 생성 +# AWS 키 페어 생성 (퍼블릭 키는 tls_private_key 리소스에서 직접 가져옴) resource "aws_key_pair" "key_pair" { key_name = local.key_name - public_key = file("${local.key_file_path}.pub") + public_key = tls_private_key.ssh.public_key_openssh +} - depends_on = [local_file.private_key] +# 로컬 환경에서만 PEM 파일 저장 (GitHub Actions에서는 실행되지 않음) +resource "local_file" "private_key" { + count = var.save_private_key_locally ? 1 : 0 + + content = tls_private_key.ssh.private_key_pem + filename = "${path.cwd}/generated/${local.key_name}.pem" + + file_permission = "0600" + + # 디렉토리 생성 + provisioner "local-exec" { + command = "mkdir -p ${path.cwd}/generated" + } } # CodeDeploy 에이전트 설치를 위한 사용자 데이터 스크립트 @@ -99,4 +84,41 @@ resource "aws_instance" "main" { Application = var.project_name ManagedBy = "terraform" } +} + +# SSH 키를 SSM Parameter Store에 저장 (안전한 접근을 위해) +resource "aws_ssm_parameter" "ec2_private_key" { + name = "/${var.project_name}/ec2/ssh/private-key" + type = "SecureString" + value = tls_private_key.ssh.private_key_pem + + description = "API 서버 SSH 프라이빗 키" + + tags = { + Name = "${var.project_name}-api-server-ssh-key" + Application = var.project_name + ManagedBy = "terraform" + } +} + +# 접속 정보도 함께 저장 +resource "aws_ssm_parameter" "ec2_connection_info" { + name = "/${var.project_name}/ec2/connection/info" + type = "String" + value = jsonencode({ + public_ip = aws_instance.main.public_ip + private_ip = aws_instance.main.private_ip + instance_id = aws_instance.main.id + key_name = aws_key_pair.key_pair.key_name + user = "ec2-user" + ssh_command = "ssh -i ec2-user@${aws_instance.main.public_ip}" + }) + + description = "API 서버 접속 정보" + + tags = { + Name = "${var.project_name}-api-server-connection-info" + Application = var.project_name + ManagedBy = "terraform" + } } \ No newline at end of file diff --git a/modules/ec2/outputs.tf b/modules/ec2/outputs.tf index f4b2c28..6dd4655 100644 --- a/modules/ec2/outputs.tf +++ b/modules/ec2/outputs.tf @@ -13,12 +13,13 @@ output "private_ip" { value = aws_instance.main.private_ip } -output "private_key_path" { - description = "로컬에 저장된 프라이빗 키 경로" - value = local.key_file_path +output "key_pair_name" { + description = "SSH 키 페어 이름" + value = aws_key_pair.key_pair.key_name } -output "ssh_command" { - description = "SSH 접속 명령어" - value = "ssh -i ${local.key_file_path} ec2-user@${aws_instance.main.public_ip}" -} \ No newline at end of file +output "private_key_pem" { + description = "프라이빗 키 (PEM 형식)" + value = tls_private_key.ssh.private_key_pem + sensitive = true +} \ No newline at end of file diff --git a/modules/ec2/variables.tf b/modules/ec2/variables.tf index 7063df9..479d76b 100644 --- a/modules/ec2/variables.tf +++ b/modules/ec2/variables.tf @@ -30,3 +30,9 @@ variable "security_group_id" { description = "EC2 인스턴스에 적용할 보안 그룹 ID" type = string } + +variable "save_private_key_locally" { + description = "프라이빗 키를 로컬에 저장할지 여부 (GitHub Actions에서는 false)" + type = bool + default = false +} diff --git a/outputs.tf b/outputs.tf index 0aee7fc..90a7366 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,6 +1,59 @@ -output "ssh_command" { - description = "EC2 instance SSH 접속 명령어" - value = module.ec2.ssh_command +# SSH 키를 Terraform 출력으로 제공 (필요시 추출 가능) +output "ec2_private_key_pem" { + description = "API 서버 SSH 프라이빗 키 (임시 접속용)" + value = module.ec2.private_key_pem + sensitive = true +} + +output "ai_server_private_key_pem" { + description = "AI 서버 SSH 프라이빗 키 (임시 접속용)" + value = module.ec2-ai.private_key_pem + sensitive = true +} + +# IP 주소 정보 +output "ec2_public_ip" { + description = "API 서버 퍼블릭 IP" + value = module.ec2.public_ip +} + +output "ai_server_public_ip" { + description = "AI 서버 퍼블릭 IP" + value = module.ec2-ai.public_ip +} + +# SSM Parameter Store에서 SSH 키 접근 방법 안내 +output "ssh_access_guide" { + description = "SSH 접속 방법 안내 (SSM Parameter Store 활용)" + value = { + message = "SSH 키가 SSM Parameter Store에 안전하게 저장되었습니다." + + api_server_access = { + step1 = "aws ssm get-parameter --name '/${local.project_name}/ec2/ssh/private-key' --with-decryption --query 'Parameter.Value' --output text > api_server.pem" + step2 = "chmod 600 api_server.pem" + step3 = "ssh -i api_server.pem ec2-user@${module.ec2.public_ip}" + cleanup = "rm -f api_server.pem" + } + + ai_server_access = { + step1 = "aws ssm get-parameter --name '/${local.project_name}/ec2-ai/ssh/private-key' --with-decryption --query 'Parameter.Value' --output text > ai_server.pem" + step2 = "chmod 600 ai_server.pem" + step3 = "ssh -i ai_server.pem ec2-user@${module.ec2-ai.public_ip}" + cleanup = "rm -f ai_server.pem" + } + + connection_info = { + api_server = "aws ssm get-parameter --name '/${local.project_name}/ec2/connection/info' --query 'Parameter.Value' --output text | jq ." + ai_server = "aws ssm get-parameter --name '/${local.project_name}/ec2-ai/connection/info' --query 'Parameter.Value' --output text | jq ." + } + + session_manager = { + api_server = "aws ssm start-session --target ${module.ec2.instance_id}" + ai_server = "aws ssm start-session --target ${module.ec2-ai.instance_id}" + } + + note = "Session Manager를 사용하면 SSH 키 없이도 접속 가능합니다." + } } output "mysql_connection_command" { @@ -19,10 +72,10 @@ output "ai_server_private_ip" { value = module.ec2-ai.private_ip } -output "ai_server_ssh_command" { - description = "AI 서버 SSH 접속 명령어 (API 서버를 통한 터널링 필요)" - value = module.ec2-ai.ssh_command -} +# output "ai_server_ssh_command" { +# description = "AI 서버 SSH 접속 명령어 (API 서버를 통한 터널링 필요)" +# value = module.ec2-ai.ssh_command +# } # 프론트엔드 관련 출력값 추가 output "frontend_s3_bucket" {