#!/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 "$@"