ai_invest/imai-invest-manager-AI/start_dist.sh

682 lines
22 KiB
Bash
Raw Normal View History

#!/bin/bash
# -*- coding: utf-8 -*-
# =============================================================================
# I'm AI 투자매니저 - AWS Lambda 배포 스크립트 (Rocky Linux 10용)
# =============================================================================
# 이 스크립트는 React 프론트엔드를 AWS Lambda로 자동 배포합니다.
# 아키텍처: CloudFront -> Lambda SSR + S3 Static Assets
# =============================================================================
set -e # 에러 발생 시 스크립트 중단
# =============================================================================
# 설정 변수
# =============================================================================
PROJECT_NAME="imai-invest-manager-ai"
AWS_REGION="ap-northeast-2" # 서울 리전
S3_BUCKET_NAME="${PROJECT_NAME}-static-assets"
LAMBDA_FUNCTION_NAME="${PROJECT_NAME}-ssr"
API_GATEWAY_NAME="${PROJECT_NAME}-api"
CLOUDFRONT_DISTRIBUTION_ID="" # 첫 배포 후 수동 설정 필요
# 버전 관리 설정
VERSION_DIR="dist_version"
BUILD_TIMESTAMP=$(date +"%y%m%d%H%M%S")
VERSIONED_DIST="${BUILD_TIMESTAMP}_dist"
# AWS 자격증명 설정 (우선순위: 환경변수 > 파라미터 > aws configure)
AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}"
AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}"
# Lambda 설정
LAMBDA_RUNTIME="nodejs20.x"
LAMBDA_HANDLER="index.handler"
LAMBDA_MEMORY=512
LAMBDA_TIMEOUT=30
# 색상 코드
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# =============================================================================
# 유틸리티 함수
# =============================================================================
log_info() {
echo -e "${BLUE}[정보]${NC} $1"
}
log_success() {
echo -e "${GREEN}[성공]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[경고]${NC} $1"
}
log_error() {
echo -e "${RED}[오류]${NC} $1"
}
load_env_file() {
ENV_FILE=".env"
if [ -f "$ENV_FILE" ]; then
log_info ".env 파일을 로드하고 있습니다..."
# .env 파일에서 AWS 키 추출
AWS_ACCESS_KEY_ID=$(grep "^AWS_YOUR_ACCESS_KEY=" "$ENV_FILE" | cut -d '=' -f2 | tr -d ' "'"'"'')
AWS_SECRET_ACCESS_KEY=$(grep "^AWS_YOUR_SECRET_KEY=" "$ENV_FILE" | cut -d '=' -f2 | tr -d ' "'"'"'')
if [ -n "$AWS_ACCESS_KEY_ID" ] && [ -n "$AWS_SECRET_ACCESS_KEY" ]; then
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
log_success ".env 파일에서 AWS 키를 로드했습니다."
return 0
else
log_warning ".env 파일에 AWS 키가 올바르게 설정되지 않았습니다."
return 1
fi
else
log_warning ".env 파일이 존재하지 않습니다."
return 1
fi
}
setup_aws_credentials() {
log_info "AWS 자격증명 설정 중..."
# 1순위: .env 파일에서 로드
if load_env_file; then
log_info ".env 파일로부터 AWS 키를 사용합니다."
# 2순위: 스크립트 파라미터로 키가 전달된 경우
elif [ "$#" -ge 2 ]; then
export AWS_ACCESS_KEY_ID="$1"
export AWS_SECRET_ACCESS_KEY="$2"
log_info "스크립트 파라미터로부터 AWS 키를 설정했습니다."
# 3순위: 환경변수가 설정된 경우
elif [ -n "$AWS_ACCESS_KEY_ID" ] && [ -n "$AWS_SECRET_ACCESS_KEY" ]; then
log_info "환경변수로부터 AWS 키를 사용합니다."
# 4순위: 대화형으로 키 입력받기
elif [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
log_warning "AWS 자격증명이 설정되지 않았습니다."
echo -n "AWS Access Key ID를 입력하세요: "
read -r AWS_ACCESS_KEY_ID
echo -n "AWS Secret Access Key를 입력하세요: "
read -rs AWS_SECRET_ACCESS_KEY
echo "" # 줄바꿈
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
log_info "대화형으로 AWS 키를 설정했습니다."
fi
# AWS 자격증명 검증
if ! aws sts get-caller-identity &> /dev/null; then
log_error "AWS 자격증명 검증에 실패했습니다. .env 파일의 키를 확인해주세요."
log_error "현재 .env 파일 위치: $(pwd)/.env"
exit 1
fi
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
log_success "AWS 계정 인증 완료: $AWS_ACCOUNT_ID"
}
check_dependencies() {
log_info "의존성 확인 중..."
# AWS CLI 확인
if ! command -v aws &> /dev/null; then
log_error "AWS CLI가 설치되어 있지 않습니다. 먼저 설치해주세요."
log_info "설치: curl \"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\" && unzip awscliv2.zip && sudo ./aws/install"
exit 1
fi
# Node.js 확인
if ! command -v node &> /dev/null; then
log_error "Node.js가 설치되어 있지 않습니다. 먼저 설치해주세요."
log_info "설치: sudo dnf module install nodejs:20/common"
exit 1
fi
# npm 확인
if ! command -v npm &> /dev/null; then
log_error "npm이 설치되어 있지 않습니다. 먼저 설치해주세요."
exit 1
fi
# zip 확인
if ! command -v zip &> /dev/null; then
log_error "zip이 설치되어 있지 않습니다. 먼저 설치해주세요."
log_info "설치: sudo dnf install -y zip unzip"
exit 1
fi
log_success "모든 의존성이 확인되었습니다."
}
# =============================================================================
# 빌드 함수
# =============================================================================
build_frontend() {
log_info "프론트엔드 빌드 시작..."
# package.json 확인
if [ ! -f "package.json" ]; then
log_error "package.json 파일을 찾을 수 없습니다. 올바른 디렉토리에서 실행하세요."
exit 1
fi
# 의존성 설치
log_info "NPM 패키지 설치 중..."
npm ci
# 빌드 실행
log_info "Vite 빌드 실행 중..."
npm run build
# dist 폴더 확인
if [ ! -d "dist" ]; then
log_error "빌드 실패: dist 폴더가 생성되지 않았습니다."
exit 1
fi
log_success "프론트엔드 빌드가 완료되었습니다."
# 버전별 dist 디렉토리 생성 및 백업
create_version_backup
}
create_version_backup() {
log_info "빌드 버전 백업 생성 중..."
# dist_version 디렉토리 생성
mkdir -p "$VERSION_DIR"
# 기존 dist를 타임스탬프명으로 복사
if [ -d "dist" ]; then
cp -r dist "$VERSION_DIR/$VERSIONED_DIST"
log_success "빌드 버전이 백업되었습니다: $VERSION_DIR/$VERSIONED_DIST"
# 버전 정보 파일 생성
cat > "$VERSION_DIR/$VERSIONED_DIST/VERSION_INFO.txt" << EOF
빌드 시간: $(date '+%Y-%m-%d %H:%M:%S')
빌드 버전: $VERSIONED_DIST
프로젝트: $PROJECT_NAME
AWS 리전: $AWS_REGION
빌드 명령: npm run build
EOF
log_info "버전 정보가 저장되었습니다: $VERSION_DIR/$VERSIONED_DIST/VERSION_INFO.txt"
# 기존 버전들 확인
VERSION_COUNT=$(ls -1 "$VERSION_DIR" | wc -l)
log_info "$VERSION_COUNT 개의 빌드 버전이 관리되고 있습니다."
# 10개 이상의 버전이 있으면 오래된 것 삭제
if [ "$VERSION_COUNT" -gt 10 ]; then
OLD_VERSIONS=$(ls -1t "$VERSION_DIR" | tail -n +11)
for old_ver in $OLD_VERSIONS; do
rm -rf "$VERSION_DIR/$old_ver"
log_warning "오래된 버전을 삭제했습니다: $old_ver"
done
fi
else
log_error "dist 폴더가 존재하지 않습니다."
exit 1
fi
}
create_lambda_package() {
log_info "Lambda 패키지 생성 중..."
# 임시 디렉토리 생성
TEMP_DIR="temp_lambda"
rm -rf $TEMP_DIR
mkdir -p $TEMP_DIR
# Lambda 핸들러 생성
cat > $TEMP_DIR/index.js << 'EOF'
const fs = require('fs');
const path = require('path');
// HTML 템플릿 로드
const htmlTemplate = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
exports.handler = async (event) => {
try {
// CloudFront에서 오는 요청 처리
const request = event.Records ? event.Records[0].cf.request : event;
// 기본 HTML 응답
const response = {
status: '200',
statusDescription: 'OK',
headers: {
'content-type': [{
key: 'Content-Type',
value: 'text/html; charset=utf-8'
}],
'cache-control': [{
key: 'Cache-Control',
value: 'public, max-age=300'
}],
'x-content-type-options': [{
key: 'X-Content-Type-Options',
value: 'nosniff'
}],
'x-frame-options': [{
key: 'X-Frame-Options',
value: 'DENY'
}],
'x-xss-protection': [{
key: 'X-XSS-Protection',
value: '1; mode=block'
}]
},
body: htmlTemplate
};
return response;
} catch (error) {
console.error('Lambda Error:', error);
return {
status: '500',
statusDescription: 'Internal Server Error',
headers: {
'content-type': [{
key: 'Content-Type',
value: 'text/plain'
}]
},
body: 'Internal Server Error'
};
}
};
EOF
# package.json 생성
cat > $TEMP_DIR/package.json << EOF
{
"name": "lambda-ssr",
"version": "1.0.0",
"main": "index.js",
"dependencies": {}
}
EOF
# HTML 파일 복사 (정적 에셋 경로 수정)
cp dist/index.html $TEMP_DIR/
# S3 경로로 정적 에셋 경로 수정
sed -i "s|/assets/|/static-assets/assets/|g" $TEMP_DIR/index.html
sed -i "s|href=\"/|href=\"/static-assets/|g" $TEMP_DIR/index.html
sed -i "s|src=\"/|src=\"/static-assets/|g" $TEMP_DIR/index.html
# Lambda 패키지 생성
cd $TEMP_DIR
zip -r ../lambda-function.zip .
cd ..
# 임시 폴더 정리
rm -rf $TEMP_DIR
log_success "Lambda 패키지가 생성되었습니다."
}
# =============================================================================
# AWS 리소스 생성/업데이트 함수
# =============================================================================
create_s3_bucket() {
log_info "S3 버킷 생성 중..."
# 버킷 존재 확인
if aws s3api head-bucket --bucket $S3_BUCKET_NAME 2>/dev/null; then
log_warning "S3 버킷이 이미 존재합니다: $S3_BUCKET_NAME"
else
# 버킷 생성
aws s3api create-bucket \
--bucket $S3_BUCKET_NAME \
--region $AWS_REGION \
--create-bucket-configuration LocationConstraint=$AWS_REGION
# 퍼블릭 액세스 차단 해제 (정적 에셋용)
aws s3api delete-public-access-block --bucket $S3_BUCKET_NAME
# 버킷 정책 설정
cat > bucket-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::$S3_BUCKET_NAME/*"
}
]
}
EOF
aws s3api put-bucket-policy --bucket $S3_BUCKET_NAME --policy file://bucket-policy.json
rm bucket-policy.json
log_success "S3 버킷이 생성되었습니다: $S3_BUCKET_NAME"
fi
}
upload_static_assets() {
log_info "정적 에셋 S3 업로드 중..."
# 정적 에셋 업로드 (cache-control 헤더 설정)
aws s3 sync dist/ s3://$S3_BUCKET_NAME/static-assets/ \
--cache-control "public, max-age=31536000" \
--exclude "index.html"
# 특별 파일들 (짧은 캐시)
if [ -f "dist/vite.svg" ]; then
aws s3 cp dist/vite.svg s3://$S3_BUCKET_NAME/static-assets/ \
--cache-control "public, max-age=86400"
fi
log_success "정적 에셋이 S3에 업로드되었습니다."
}
create_lambda_role() {
log_info "Lambda IAM 역할 확인/생성 중..."
ROLE_NAME="${PROJECT_NAME}-lambda-role"
# 역할 존재 확인
if aws iam get-role --role-name $ROLE_NAME 2>/dev/null; then
log_warning "IAM 역할이 이미 존재합니다: $ROLE_NAME"
echo "arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):role/$ROLE_NAME"
return
fi
# 신뢰 정책 생성
cat > trust-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com",
"edgelambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
EOF
# 역할 생성
aws iam create-role \
--role-name $ROLE_NAME \
--assume-role-policy-document file://trust-policy.json
# 권한 정책 연결
aws iam attach-role-policy \
--role-name $ROLE_NAME \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# S3 읽기 권한 추가
cat > s3-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::$S3_BUCKET_NAME/*"
}
]
}
EOF
aws iam put-role-policy \
--role-name $ROLE_NAME \
--policy-name S3ReadPolicy \
--policy-document file://s3-policy.json
rm trust-policy.json s3-policy.json
# 역할이 생성될 때까지 대기
sleep 10
ROLE_ARN="arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):role/$ROLE_NAME"
log_success "IAM 역할이 생성되었습니다: $ROLE_ARN"
echo $ROLE_ARN
}
deploy_lambda() {
log_info "Lambda 함수 배포 중..."
ROLE_ARN=$(create_lambda_role)
# 함수 존재 확인
if aws lambda get-function --function-name $LAMBDA_FUNCTION_NAME 2>/dev/null; then
log_info "기존 Lambda 함수 업데이트 중..."
# 함수 코드 업데이트
aws lambda update-function-code \
--function-name $LAMBDA_FUNCTION_NAME \
--zip-file fileb://lambda-function.zip
# 함수 구성 업데이트
aws lambda update-function-configuration \
--function-name $LAMBDA_FUNCTION_NAME \
--runtime $LAMBDA_RUNTIME \
--handler $LAMBDA_HANDLER \
--memory-size $LAMBDA_MEMORY \
--timeout $LAMBDA_TIMEOUT
else
log_info "새 Lambda 함수 생성 중..."
# 함수 생성
aws lambda create-function \
--function-name $LAMBDA_FUNCTION_NAME \
--runtime $LAMBDA_RUNTIME \
--role $ROLE_ARN \
--handler $LAMBDA_HANDLER \
--zip-file fileb://lambda-function.zip \
--memory-size $LAMBDA_MEMORY \
--timeout $LAMBDA_TIMEOUT \
--description "I'm AI Invest Manager Frontend SSR"
fi
# Lambda 함수가 활성화될 때까지 대기
aws lambda wait function-active --function-name $LAMBDA_FUNCTION_NAME
log_success "Lambda 함수가 배포되었습니다."
}
create_api_gateway() {
log_info "API Gateway 생성 중..."
# API Gateway 존재 확인
API_ID=$(aws apigateway get-rest-apis --query "items[?name=='$API_GATEWAY_NAME'].id" --output text)
if [ -n "$API_ID" ] && [ "$API_ID" != "None" ]; then
log_warning "API Gateway가 이미 존재합니다: $API_ID"
else
# API Gateway 생성
API_ID=$(aws apigateway create-rest-api \
--name $API_GATEWAY_NAME \
--description "I'm AI Invest Manager API" \
--query 'id' --output text)
log_info "API Gateway ID: $API_ID"
# 루트 리소스 가져오기
ROOT_RESOURCE_ID=$(aws apigateway get-resources \
--rest-api-id $API_ID \
--query 'items[?path==`/`].id' --output text)
# Proxy 리소스 생성
RESOURCE_ID=$(aws apigateway create-resource \
--rest-api-id $API_ID \
--parent-id $ROOT_RESOURCE_ID \
--path-part '{proxy+}' \
--query 'id' --output text)
# Lambda 통합을 위한 ARN
LAMBDA_ARN="arn:aws:lambda:$AWS_REGION:$(aws sts get-caller-identity --query Account --output text):function:$LAMBDA_FUNCTION_NAME"
# ANY 메서드 생성
aws apigateway put-method \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method ANY \
--authorization-type NONE
# Lambda 통합 설정
aws apigateway put-integration \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method ANY \
--type AWS_PROXY \
--integration-http-method POST \
--uri "arn:aws:apigateway:$AWS_REGION:lambda:path/2015-03-31/functions/$LAMBDA_ARN/invocations"
# Lambda 권한 추가
aws lambda add-permission \
--function-name $LAMBDA_FUNCTION_NAME \
--statement-id apigateway-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:$AWS_REGION:$(aws sts get-caller-identity --query Account --output text):$API_ID/*/*/*" \
2>/dev/null || true
# 배포
aws apigateway create-deployment \
--rest-api-id $API_ID \
--stage-name prod
log_success "API Gateway가 생성되었습니다: $API_ID"
fi
API_URL="https://$API_ID.execute-api.$AWS_REGION.amazonaws.com/prod"
log_info "API Gateway URL: $API_URL"
}
invalidate_cloudfront() {
if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then
log_info "CloudFront 캐시 무효화 중..."
INVALIDATION_ID=$(aws cloudfront create-invalidation \
--distribution-id $CLOUDFRONT_DISTRIBUTION_ID \
--paths "/*" \
--query 'Invalidation.Id' --output text)
log_success "CloudFront 무효화 요청이 생성되었습니다: $INVALIDATION_ID"
else
log_warning "CLOUDFRONT_DISTRIBUTION_ID가 설정되지 않았습니다. 수동으로 CloudFront를 설정해주세요."
fi
}
cleanup() {
log_info "임시 파일 정리 중..."
rm -f lambda-function.zip
log_success "정리가 완료되었습니다."
}
# =============================================================================
# 메인 실행 함수
# =============================================================================
show_usage() {
echo "사용법: $0 [AWS_ACCESS_KEY_ID] [AWS_SECRET_ACCESS_KEY]"
echo ""
echo "AWS 자격증명 제공 방법 (우선순위 순):"
echo "1. .env 파일: 프로젝트 루트의 .env 파일에서 자동 로드"
echo "2. 스크립트 파라미터: ./start_dist.sh ACCESS_KEY SECRET_KEY"
echo "3. 환경변수: export AWS_ACCESS_KEY_ID=... && export AWS_SECRET_ACCESS_KEY=..."
echo "4. 대화형 입력: 스크립트 실행 중 키 입력"
echo "5. AWS CLI 프로필: aws configure로 미리 설정"
echo ""
echo ".env 파일 형식:"
echo " [AWS KEY INFO]"
echo " AWS_YOUR_ACCESS_KEY=AKIA1234567890EXAMPLE"
echo " AWS_YOUR_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
echo ""
echo "예제:"
echo " # .env 파일 사용 (권장)"
echo " ./start_dist.sh"
echo ""
echo " # 파라미터 직접 전달"
echo " ./start_dist.sh AKIA... wJalr..."
echo ""
}
main() {
# 도움말 표시
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
show_usage
exit 0
fi
log_info "=== I'm AI 투자매니저 AWS Lambda 배포 시작 ==="
log_info "프로젝트: $PROJECT_NAME"
log_info "AWS 리전: $AWS_REGION"
log_info "현재 디렉토리: $(pwd)"
log_info "빌드 버전: $VERSIONED_DIST"
echo ""
# 의존성 확인
check_dependencies
# AWS 자격증명 설정
setup_aws_credentials "$@"
# 빌드 및 패키징
build_frontend
create_lambda_package
# AWS 리소스 생성/업데이트
create_s3_bucket
upload_static_assets
deploy_lambda
create_api_gateway
# 캐시 무효화
invalidate_cloudfront
# 정리
cleanup
echo ""
log_success "=== 배포가 완료되었습니다! ==="
log_info "빌드 버전: $VERSIONED_DIST"
log_info "백업 위치: $VERSION_DIR/$VERSIONED_DIST"
log_info "S3 버킷: https://s3.console.aws.amazon.com/s3/buckets/$S3_BUCKET_NAME"
log_info "Lambda 함수: https://$AWS_REGION.console.aws.amazon.com/lambda/home?region=$AWS_REGION#/functions/$LAMBDA_FUNCTION_NAME"
if [ -n "$API_ID" ]; then
log_info "API Gateway: https://$API_ID.execute-api.$AWS_REGION.amazonaws.com/prod"
fi
echo ""
log_warning "CloudFront 설정이 필요합니다:"
log_info "1. AWS Console에서 CloudFront Distribution 생성"
log_info "2. Origin: API Gateway (SSR) + S3 (Static Assets)"
log_info "3. CLOUDFRONT_DISTRIBUTION_ID 변수에 Distribution ID 설정"
echo ""
log_success "배포가 성공적으로 완료되었습니다."
}
# 스크립트 실행
main "$@"