인프라 구축기
현재 인프라를 사용하면서 지속적으로 비용이 청구되고 있다. Redshift Serverless는 가변 비용으로 쿼리 비용만을 청구하며 Bastion Host EC2는 프리티어이므로 비용이 발생하지 않지만, Airflow EC2와 RDS의 경우 서버를 사용하기에 켜두기만 하더라도 비용이 발생한다. 특히 Airflow EC2의 경우 t3.large의 인스턴스를 사용하고 외부와의 통신 (web, API 등)이 자주 이루어지므로 추가로 비용이 발생한다. 그래서 서버를 사용하지 않는 시간에는 서비스를 중지하여 비용을 절약하고자 하였다.
- 사용하지 않을 때 중지할 서비스 : EC2 (Bastion Host, Airflow), RDS
- Redshfit의 경우 사용한 만큼만 비용이 청구되므로 중지하지 않아도 됨
인스턴스 시작, 중지 자동화 개요 (Feat. lambda)
AWS lambda function
- Serverless 환경에서 자동으로 코드를 실행할 수 있는 서비스
- GB-초 당 $0.0000167, 요청 1백만 건당 $0.2 : 합리적인 가격으로 자동화 가능 (가격 페이지)
- AWS CloudWatch와 연동해 특정 시간마다 lambda에 등록된 코드를 Trigger 할 수 있음
lambda 적용
- 시작 : UTC +9 (한국) 기준, 오전 9시에 사용 중인 EC2, RDS 서버 시작
- 중지 : UTC +9 (한국) 기준, 오후 9시에 사용 중인 EC2, RDS 서버 중지
- 이후 Airflow DAG의 작동 시간에 따라 조정될 수 있으나 임시로 09 ~ 21시로 지정
lambda 적용 이후 디렉터리 구조
aws_infra
│
│ main.tf
│
├───instance
│ ec2.tf
│ output.tf
│ variables.tf
│
├───lambda
│ daily_start_service.py
│ daily_start_service.zip
│ daily_stop_service.py
│ daily_stop_service.zip
│ iam_role.tf
│ lambda_start.tf
│ lambda_stop.tf
│ variables.tf
│
├───storage
│ output.tf
│ rds.tf
│ redshift_serverless.tf
│ s3.tf
│ variables.tf
│
└───vpc
internet_gateway.tf
nat_gateway.tf
output.tf
route_table.tf
s3_endpoint.tf
security_group.tf
subnet.tf
vpc.tf
기존 모듈 수정 사항
작업 요약
- EC2, RDS의 ID를 통해 lambda에서 연결되므로 output.tf로 lambda 모듈로 넘겨줘야 함
- instance/output.tf : 사용 중인 EC2 (Bastion Host, Airflow)의 ID
- storage/output.tf : 사용 중인 RDS (Airflow Meta DB)의 ID
- 인스턴스가 재시작되면 Bastion Host의 Public IP가 변경되므로 고정된 IP가 필요
- Bastion Host에 Elastic IP를 적용해 고정된 IP로 Airflow Web Server에 접속 가능
- 새로운 모듈 (lambda)을 생성하므로 main.tf에 추가
instance/ec2.tf
- Bastion Host에 Elastic IP를 적용해 고정된 IP로 Airflow Web Server에 접속 가능
resource "aws_eip" "bastion_host_eip" {
domain = "vpc"
tags = {
Name = "popboard-bastion-host-eip"
}
}
resource "aws_instance" "bastion_host" {
ami = "ami-040c33c6a51fd5d96"
instance_type = "t2.micro"
subnet_id = var.public_subnet_2a_1_id
key_name = "popboard-bastion-host-keypair"
...
}
# bastion host eip association
resource "aws_eip_association" "bastion_host_eip_association" {
instance_id = aws_instance.bastion_host.id
allocation_id = aws_eip.bastion_host_eip.id
}
instance/output.tf
- 사용 중인 EC2 (Bastion Host, Airflow)의 ID
output "bastion_host_id" {
value = aws_instance.bastion_host.id
}
output "airflow_id" {
value = aws_instance.airflow.id
}
storage/output.tf
- 사용 중인 RDS (Airflow Meta DB)의 ID
output "airflow_meta_db_id" {
value = aws_db_instance.airflow_meta_db.identifier
}
main.tf
- 새로운 모듈 (lambda)의 생성하므로 main.tf에 추가
...
module "lambda" {
depends_on = [module.instance]
source = "./lambda"
# variables
airflow_id = module.instance.airflow_id
bastion_host_id = module.instance.bastion_host_id
airflow_meta_db_id = module.storage.airflow_meta_db_id
}
lambda 모듈 추가
작업 요약
- AWS 인스턴스를 중지하고 시작하기 위한 lambda function 코드 작성
- daily_start_service.py : UTC +9 (한국) 기준, 오전 9시에 RDS, EC2 인스턴스 시작
- daily_stop_service.py : UTC +9 (한국) 기준, 오후 9시에 RDS, EC2 인스턴스 중지
- .zip 파일을 lambda function의 인자로 넘겨야 하기에 압축 파일도 만들어야 함
- lambda function에서 ec2, rds에 접근하기 위해 IAM Role 적용
- AmazonEC2FullAccess, AmazonRDSFullAccess
- lambda 및 CloudWatch 서비스 생성
- 인스턴스 시작, 중지를 구분하여 총 2개의 lambda 서비스 생성
- lambda function에서 연결할 인스턴스의 ID가 필요
- 각 인스턴스의 ID를 variable로 가져와 lambda의 환경변수로 넘김
daily_start_service.py
- UTC +9 (한국) 기준, 오전 9시에 RDS, EC2 인스턴스 시작
- 예외 처리
- 지정한 환경 변수가 존재하지 않을 경우
- 해당 ID의 인스턴스가 이미 중지 상태일 경우
- logger를 통해 효과적으로 대처할 수 있도록 설정
import boto3
import os
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
ec2 = boto3.client('ec2', region_name='ap-northeast-2')
rds = boto3.client('rds', region_name='ap-northeast-2')
messages = []
# 환경 변수 확인
try:
instance_ids = os.environ['INSTANCE_IDS'].split(',')
if not instance_ids or instance_ids == ['']:
raise ValueError("INSTANCE_IDS environment variable is not set correctly.")
except Exception as e:
logger.error(f"Error retrieving INSTANCE_IDS: {e}")
return f"Failed to retrieve INSTANCE_IDS: {e}"
try:
rds_instance_id = os.environ['RDS_INSTANCE_ID']
if not rds_instance_id:
raise ValueError("RDS_INSTANCE_ID environment variable is not set.")
except Exception as e:
logger.error(f"Error retrieving RDS_INSTANCE_ID: {e}")
return f"Failed to retrieve RDS_INSTANCE_ID: {e}"
# EC2 인스턴스 상태 확인 및 시작
try:
response = ec2.describe_instances(InstanceIds=instance_ids)
statuses = [instance['State']['Name'] for reservation in response['Reservations'] for instance in reservation['Instances']]
if all(status == 'stopped' for status in statuses):
ec2.start_instances(InstanceIds=instance_ids)
logger.info(f"Starting instances: {instance_ids}")
messages.append(f"Started EC2 instances: {instance_ids}")
else:
logger.info(f"EC2 instances are already running: {instance_ids}")
messages.append(f"EC2 instances are already running: {instance_ids}")
except Exception as e:
logger.error(f"Error checking or starting EC2 instances: {e}")
messages.append(f"Failed to check or start EC2 instances: {e}")
# RDS 인스턴스 상태 확인 및 시작
try:
response = rds.describe_db_instances(DBInstanceIdentifier=rds_instance_id)
status = response['DBInstances'][0]['DBInstanceStatus']
if status == 'stopped':
rds.start_db_instance(DBInstanceIdentifier=rds_instance_id)
logger.info(f"Starting DB instance: {rds_instance_id}")
messages.append(f"Started RDS instance: {rds_instance_id}")
else:
logger.info(f"RDS instance {rds_instance_id} is already {status}.")
messages.append(f"RDS instance {rds_instance_id} is already {status}.")
except Exception as e:
logger.error(f"Error checking or starting RDS instance: {e}")
messages.append(f"Failed to check or start RDS instance: {e}")
return "\n".join(messages)
daily_stop_service.py
- UTC +9 (한국) 기준, 오후 9시에 RDS, EC2 인스턴스 중지
- 예외 처리
- 지정한 환경 변수가 존재하지 않을 경우
- 해당 ID의 인스턴스가 이미 시작 상태일 경우
- logger를 통해 효과적으로 대처할 수 있도록 설정
import boto3
import os
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
ec2 = boto3.client('ec2', region_name='ap-northeast-2')
rds = boto3.client('rds', region_name='ap-northeast-2')
messages = []
# 환경 변수 확인
try:
instance_ids = os.environ['INSTANCE_IDS'].split(',')
if not instance_ids or instance_ids == ['']:
raise ValueError("INSTANCE_IDS environment variable is not set correctly.")
except Exception as e:
logger.error(f"Error retrieving INSTANCE_IDS: {e}")
return f"Failed to retrieve INSTANCE_IDS: {e}"
try:
rds_instance_id = os.environ['RDS_INSTANCE_ID']
if not rds_instance_id:
raise ValueError("RDS_INSTANCE_ID environment variable is not set.")
except Exception as e:
logger.error(f"Error retrieving RDS_INSTANCE_ID: {e}")
return f"Failed to retrieve RDS_INSTANCE_ID: {e}"
# EC2 인스턴스 상태 확인 및 종료
try:
response = ec2.describe_instances(InstanceIds=instance_ids)
statuses = [instance['State']['Name'] for reservation in response['Reservations'] for instance in reservation['Instances']]
if any(status == 'running' for status in statuses):
ec2.stop_instances(InstanceIds=instance_ids)
logger.info(f"Stopping instances: {instance_ids}")
messages.append(f"Stopped EC2 instances: {instance_ids}")
else:
logger.info(f"EC2 instances are already stopped: {instance_ids}")
messages.append(f"EC2 instances are already stopped: {instance_ids}")
except Exception as e:
logger.error(f"Error checking or stopping EC2 instances: {e}")
messages.append(f"Failed to check or stop EC2 instances: {e}")
# RDS 인스턴스 상태 확인 및 종료
try:
response = rds.describe_db_instances(DBInstanceIdentifier=rds_instance_id)
status = response['DBInstances'][0]['DBInstanceStatus']
if status == 'available':
rds.stop_db_instance(DBInstanceIdentifier=rds_instance_id)
logger.info(f"Stopping DB instance: {rds_instance_id}")
messages.append(f"Stopped RDS instance: {rds_instance_id}")
else:
logger.info(f"RDS instance {rds_instance_id} is already {status}.")
messages.append(f"RDS instance {rds_instance_id} is already {status}.")
except Exception as e:
logger.error(f"Error checking or stopping RDS instance: {e}")
messages.append(f"Failed to check or stop RDS instance: {e}")
return "\n".join(messages)
iam_role.tf
- lambda function에서 ec2, rds에 접근하기 위해 IAM Role 적용
resource "aws_iam_role" "lambda_exec_role" {
name = "popboard-lambda-exec-role"
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
})
}
resource "aws_iam_policy_attachment" "lambda_ec2_policy" {
name = "popboard-lambda-ec2-policy"
roles = [aws_iam_role.lambda_exec_role.name]
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
}
resource "aws_iam_policy_attachment" "lambda_rds_policy" {
name = "popboard-lambda-rds-policy"
roles = [aws_iam_role.lambda_exec_role.name]
policy_arn = "arn:aws:iam::aws:policy/AmazonRDSFullAccess"
}
lambda_start.tf
- 인스턴스 시작을 위한 lambda 및 CloudWatch 서비스 생성
resource "aws_lambda_function" "start_aws_service" {
function_name = "popboard-start-aws-service"
role = aws_iam_role.lambda_exec_role.arn
handler = "daily_start_service.lambda_handler"
runtime = "python3.9"
filename = "${path.module}/daily_start_service.zip"
source_code_hash = filebase64sha256("${path.module}/daily_start_service.zip")
environment {
variables = {
INSTANCE_IDS = "${var.airflow_id},${var.bastion_host_id}"
RDS_INSTANCE_ID = var.airflow_meta_db_id
}
}
}
resource "aws_cloudwatch_event_rule" "start_aws_service_schedule" {
name = "popboard-start-aws-service-schedule"
description = "Start AWS services at 9 AM (UTC+9)"
schedule_expression = "cron(0 0 * * ? *)"
}
resource "aws_cloudwatch_event_target" "start_aws_service_event_target" {
rule = aws_cloudwatch_event_rule.start_aws_service_schedule.name
target_id = "popboard-start-aws-service-lambda"
arn = aws_lambda_function.start_aws_service.arn
}
resource "aws_lambda_permission" "start_aws_service_allow_cloudwatch_to_invoke" {
statement_id = "popboard-start-aws-service-allow-cloudwatch-to-invoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.start_aws_service.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.start_aws_service_schedule.arn
}
lambda_stop.tf
- 인스턴스 중지를 위한 lambda 및 CloudWatch 서비스 생성
resource "aws_lambda_function" "stop_aws_service" {
function_name = "popboard-stop-aws-service"
role = aws_iam_role.lambda_exec_role.arn
handler = "daily_stop_service.lambda_handler"
runtime = "python3.9"
filename = "${path.module}/daily_stop_service.zip"
source_code_hash = filebase64sha256("${path.module}/daily_stop_service.zip")
environment {
variables = {
INSTANCE_IDS = "${var.airflow_id},${var.bastion_host_id}"
RDS_INSTANCE_ID = var.airflow_meta_db_id
}
}
}
resource "aws_cloudwatch_event_rule" "stop_aws_service_schedule" {
name = "popboard-stop-aws-service-schedule"
description = "Stop AWS services at 9 PM (UTC+9)"
schedule_expression = "cron(0 12 * * ? *)"
}
resource "aws_cloudwatch_event_target" "stop_aws_service_event_target" {
rule = aws_cloudwatch_event_rule.stop_aws_service_schedule.name
target_id = "popboard-stop-aws-service-lambda"
arn = aws_lambda_function.stop_aws_service.arn
}
resource "aws_lambda_permission" "stop_aws_service_allow_cloudwatch_to_invoke" {
statement_id = "popboard-stop-aws-service-allow-cloudwatch-to-invoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.stop_aws_service.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.stop_aws_service_schedule.arn
}
variables.tf
- 각 인스턴스의 ID를 variable로 가져와 lambda의 환경변수로 넘김
variable "airflow_id" {
description = "airflow ec2 id"
type = string
}
variable "bastion_host_id" {
description = "bastion host id"
type = string
}
variable "airflow_meta_db_id" {
description = "airflow meta db id"
type = string
}
고민 과정
인스턴스 재시작 시 nginx 및 docker container 실행 여부
- nginx 실행 여부
- EC2 재시작에 따라 실행 중이던 nginx가 종료되는지 확인
- 재시작해도 올바르게 실행되므로 추가 작업이 필요 없음
service nginx status
- docker container 실행 여부
- EC2 재시작에 따라 실행 중이던 docker container가 종료되는지 확인
- 재시작해도 올바르게 실행되므로 추가 작업이 필요 없음
sudo docker ps
- 로컬에서 웹 접속 확인
- nginx의 server_name을 적절히 수정 후 nginx 재시작
- 로컬에서 수정된 Bastion Host의 IP를 통해 접속 가능
http://<Bastion_Host_Elastic_IP>
'Infra > [인프라 구축기] Terraform 활용 AWS 인프라 구축' 카테고리의 다른 글
인프라 구축기 (10) - IAM 생성 코드 추가 (2) | 2024.10.26 |
---|---|
인프라 구축기 (9) - Terraform terraform.tfstate 삭제 이슈 (0) | 2024.10.24 |
인프라 구축기 (7) - Airflow 및 Redshift 사용자 생성, 권한 설정 (1) | 2024.10.18 |
인프라 구축기 (6) - 로컬에서 Private EC2 Airflow Web Server 접속 (0) | 2024.10.17 |
인프라 구축기 (5) - Private Subnet EC2에서 다른 Subnet의 인스턴스 접근 확인 (0) | 2024.10.11 |