diff --git "a/.github/ISSUE_TEMPLATE/\342\231\273\357\270\217-refactor.md" "b/.github/ISSUE_TEMPLATE/\342\231\273\357\270\217-refactor.md" new file mode 100644 index 0000000..7b39935 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\342\231\273\357\270\217-refactor.md" @@ -0,0 +1,11 @@ +--- +name: "♻️ refactor" +about: 리팩토링 이슈 템플릿 +title: "[refactor] " +labels: "♻️ refactor" +assignees: '' + +--- + +## 📌 Description +- diff --git "a/.github/ISSUE_TEMPLATE/\342\234\250-feature.md" "b/.github/ISSUE_TEMPLATE/\342\234\250-feature.md" new file mode 100644 index 0000000..56724c1 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\342\234\250-feature.md" @@ -0,0 +1,11 @@ +--- +name: "✨ feature" +about: 기능 추가 이슈 템플릿 +title: "[feat] " +labels: "✨ feature" +assignees: '' + +--- + +## 📌 Description +- diff --git "a/.github/ISSUE_TEMPLATE/\360\237\220\233-fix.md" "b/.github/ISSUE_TEMPLATE/\360\237\220\233-fix.md" new file mode 100644 index 0000000..71ef3d6 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\220\233-fix.md" @@ -0,0 +1,11 @@ +--- +name: "\U0001F41B fix" +about: 버그 및 에러 이슈 템플릿 +title: "[fix] " +labels: "\U0001F41B fix" +assignees: '' + +--- + +## 📌 Description +- diff --git "a/.github/ISSUE_TEMPLATE/\360\237\223\235-documentation.md" "b/.github/ISSUE_TEMPLATE/\360\237\223\235-documentation.md" new file mode 100644 index 0000000..5374bfe --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\223\235-documentation.md" @@ -0,0 +1,11 @@ +--- +name: "\U0001F4DD documentation" +about: 문서화 이슈 템플릿 +title: "[docs] " +labels: "\U0001F4DD documentation" +assignees: '' + +--- + +## 📌 Description +- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8ebcb52 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ + + + + + + +## 📌 연관 이슈 + +- close # + +## 🌱 PR 요약 + + +## 🛠 작업 내용 + +- +- + +## 📸 스크린샷 + + +## ❗️리뷰어들께 + diff --git a/.github/workflows/README_DESTROY.md b/.github/workflows/README_DESTROY.md new file mode 100644 index 0000000..b737adb --- /dev/null +++ b/.github/workflows/README_DESTROY.md @@ -0,0 +1,134 @@ +# Terraform Destroy Workflows + +이 디렉토리는 AWS 인프라를 안전하게 삭제하기 위한 GitHub Actions 워크플로우를 포함합니다. + +## 🚨 주의사항 + +- **이 워크플로우들은 되돌릴 수 없는 작업을 수행합니다** +- **Production 환경 삭제 전에는 반드시 데이터베이스 백업을 완료하세요** +- **실행 전에 모든 중요한 데이터가 백업되었는지 확인하세요** + +## 📋 사용 가능한 워크플로우 + +### 1. Development 환경 삭제 (`destroy_dev.yml`) + +**사용 시나리오:** +- 개발 환경에서 테스트 후 인프라 정리 +- 개발 환경 재구성 +- 개발 환경 비용 절약 + +**실행 방법:** +1. GitHub 저장소의 Actions 탭으로 이동 +2. "Terraform Destroy Development Environment" 워크플로우 선택 +3. "Run workflow" 버튼 클릭 +4. 다음 정보 입력: + - `confirm_destroy`: "DESTROY" 입력 + - `reason`: 삭제 이유 입력 + +### 2. Production 환경 삭제 (`destroy_prod.yml`) + +**사용 시나리오:** +- 다른 AWS 계정으로 이관 시 기존 인프라 삭제 +- Production 환경 재구성 +- 서비스 종료 + +**실행 방법:** +1. GitHub 저장소의 Actions 탭으로 이동 +2. "Terraform Destroy Production Environment" 워크플로우 선택 +3. "Run workflow" 버튼 클릭 +4. 다음 정보 입력: + - `confirm_destroy`: "DESTROY_PRODUCTION" 입력 + - `reason`: 삭제 이유 입력 (필수) + - `backup_required`: 백업 상태 선택 + +### 3. 전체 환경 삭제 (`destroy_all.yml`) + +**사용 시나리오:** +- 모든 환경을 한 번에 삭제 +- 프로젝트 완전 종료 +- 전체 인프라 재구성 + +**실행 방법:** +1. GitHub 저장소의 Actions 탭으로 이동 +2. "Terraform Destroy All Environments" 워크플로우 선택 +3. "Run workflow" 버튼 클릭 +4. 다음 정보 입력: + - `confirm_destroy`: "DESTROY_ALL_ENVIRONMENTS" 입력 + - `reason`: 삭제 이유 입력 (필수) + - `backup_required`: Production 백업 상태 선택 + - `environments`: 삭제할 환경 선택 + +## 🔒 보안 고려사항 + +### 필요한 GitHub Secrets + +각 워크플로우는 다음 secrets가 설정되어 있어야 합니다: + +**Development 환경:** +- `DEV_AWS_ACCESS_KEY_ID` +- `DEV_AWS_SECRET_ACCESS_KEY` +- `DEV_DOMAIN_NAME` +- `RDS_USERNAME` +- `RDS_PASSWORD` + +**Production 환경:** +- `PROD_AWS_ACCESS_KEY_ID` +- `PROD_AWS_SECRET_ACCESS_KEY` +- `PROD_DOMAIN_NAME` +- `RDS_USERNAME` +- `RDS_PASSWORD` + +### 권한 관리 + +- Production 환경 삭제는 관리자 권한이 있는 사용자만 실행해야 합니다 +- 필요시 GitHub의 브랜치 보호 규칙을 활용하여 추가 승인 프로세스를 구현하세요 + +## 📝 실행 전 체크리스트 + +### Development 환경 삭제 전: +- [ ] 중요한 데이터가 Production에 백업되어 있는지 확인 +- [ ] 개발 중인 코드가 저장소에 커밋되어 있는지 확인 +- [ ] 다른 개발자들이 해당 환경을 사용하지 않는지 확인 + +### Production 환경 삭제 전: +- [ ] **데이터베이스 백업 완료** (필수) +- [ ] 사용자에게 서비스 중단 공지 +- [ ] 도메인 DNS 설정 백업 +- [ ] SSL 인증서 백업 (필요시) +- [ ] 모니터링 로그 백업 (필요시) +- [ ] 비즈니스 연속성 계획 수립 + +## 🛠️ 문제 해결 + +### 일반적인 오류: + +1. **"Destroy 확인이 올바르지 않습니다"** + - 정확한 확인 문자열을 입력했는지 확인 + - 대소문자 구분 주의 + +2. **"데이터베이스 백업이 완료되지 않았습니다"** + - Production 환경 삭제 시 백업 상태를 올바르게 선택했는지 확인 + +3. **AWS 권한 오류** + - GitHub Secrets의 AWS 자격 증명이 올바른지 확인 + - AWS IAM 사용자에게 필요한 권한이 있는지 확인 + +### 복구 방법: + +일반적으로 destroy 작업은 되돌릴 수 없습니다. 복구가 필요한 경우: +1. 데이터베이스 백업에서 복원 +2. Terraform 코드를 사용하여 인프라 재구성 +3. DNS 설정 수동 복원 + +## 📞 지원 + +문제가 발생하거나 도움이 필요한 경우: +1. GitHub Issues에 문제 보고 +2. 팀 채널에서 문의 +3. 인프라 관리자에게 직접 연락 + +--- + +**⚠️ 마지막 경고: 이 워크플로우들은 프로덕션 환경에서 매우 신중하게 사용해야 합니다. 실행 전에 충분한 검토와 백업을 진행하세요.** + + diff --git a/.github/workflows/cd_dev.yml b/.github/workflows/cd_dev.yml new file mode 100644 index 0000000..1b3f0dc --- /dev/null +++ b/.github/workflows/cd_dev.yml @@ -0,0 +1,51 @@ +name: Terraform Development CD + +on: + push: + branches: + - dev +jobs: + terraform-apply: + name: Terraform Apply to Dev + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.12.2 + + - name: Terraform Init (dev) + run: terraform -chdir=terraform/env/dev init + + - name: Create terraform.secret.tfvars for CD + working-directory: terraform/env/dev + run: | + cat > terraform.secret.tfvars << EOF + aws_access_key_id = "${{ secrets.DEV_AWS_ACCESS_KEY_ID }}" + aws_secret_access_key = "${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}" + rds_username = "${{ secrets.RDS_USERNAME }}" + rds_password = "${{ secrets.RDS_PASSWORD }}" + domain_name = "${{ secrets.DEV_DOMAIN_NAME }}" + EOF + + - name: Terraform Apply (dev) + working-directory: terraform/env/dev + run: | + terraform apply \ + -auto-approve \ + -input=false \ + -var-file="terraform.secret.tfvars" \ No newline at end of file diff --git a/.github/workflows/cd_prod.yml b/.github/workflows/cd_prod.yml new file mode 100644 index 0000000..1558889 --- /dev/null +++ b/.github/workflows/cd_prod.yml @@ -0,0 +1,52 @@ +name: Terraform Production CD + +on: + push: + branches: + - prod + +jobs: + terraform-apply: + name: Terraform Apply to Prod + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.12.2 + + - name: Terraform Init (prod) + run: terraform -chdir=terraform/env/prod init + + - name: Create terraform.secret.tfvars for CD + working-directory: terraform/env/prod + run: | + cat > terraform.secret.tfvars << EOF + aws_access_key_id = "${{ secrets.PROD_AWS_ACCESS_KEY_ID }}" + aws_secret_access_key = "${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }}" + rds_username = "${{ secrets.RDS_USERNAME }}" + rds_password = "${{ secrets.RDS_PASSWORD }}" + domain_name = "${{ secrets.PROD_DOMAIN_NAME }}" + EOF + + - name: Terraform Apply (prod) + working-directory: terraform/env/prod + run: | + terraform apply \ + -auto-approve \ + -input=false \ + -var-file="terraform.secret.tfvars" \ No newline at end of file diff --git a/.github/workflows/ci_dev.yml b/.github/workflows/ci_dev.yml new file mode 100644 index 0000000..d9797cf --- /dev/null +++ b/.github/workflows/ci_dev.yml @@ -0,0 +1,92 @@ +name: Terraform Development CI + +on: + pull_request: + branches: [ dev ] + +jobs: + terraform: + name: Terraform Format, Validate, Plan + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.12.2 + + - name: Terraform Init (dev) + run: terraform -chdir=terraform/env/dev init + + - name: Terraform Format Check (전체) + run: terraform fmt -check -recursive + + - name: Create terraform.secret.tfvars for CI + working-directory: terraform/env/dev + run: | + cat > terraform.secret.tfvars << EOF + aws_access_key_id = "${{ secrets.DEV_AWS_ACCESS_KEY_ID }}" + aws_secret_access_key = "${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}" + rds_username = "${{ secrets.RDS_USERNAME }}" + rds_password = "${{ secrets.RDS_PASSWORD }}" + domain_name = "${{ secrets.DEV_DOMAIN_NAME }}" + EOF + + - name: Terraform Plan (dev) + id: plan + working-directory: terraform/env/dev + run: | + # terraform plan 실행 + terraform plan \ + -input=false \ + -no-color \ + -var-file="terraform.secret.tfvars" \ + > plan.txt + + - name: Delete previous Terraform plan comments + uses: actions/github-script@v7 + with: + script: | + const planTag = "## 📝 Terraform Plan Result (dev)"; + const comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + + for (const comment of comments.data) { + if (comment.body && comment.body.startsWith(planTag)) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }); + } + } + + - name: Comment PR with plan output + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const plan = fs.readFileSync('terraform/env/dev/plan.txt', 'utf8'); + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## 📝 Terraform Plan Result (dev)\n\n\`\`\`terraform\n${plan}\n\`\`\`` + }); \ No newline at end of file diff --git a/.github/workflows/ci_prod.yml b/.github/workflows/ci_prod.yml new file mode 100644 index 0000000..d14ea87 --- /dev/null +++ b/.github/workflows/ci_prod.yml @@ -0,0 +1,91 @@ +name: Terraform Production CI + +on: + pull_request: + branches: [ prod ] + +jobs: + terraform: + name: Terraform Format, Validate, Plan + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.12.2 + + - name: Terraform Init (prod) + run: terraform -chdir=terraform/env/prod init + + - name: Terraform Format Check (전체 코드 기준) + run: terraform fmt -check -recursive + + - name: Create terraform.secret.tfvars for CI + working-directory: terraform/env/prod + run: | + cat > terraform.secret.tfvars << EOF + aws_access_key_id = "${{ secrets.PROD_AWS_ACCESS_KEY_ID }}" + aws_secret_access_key = "${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }}" + rds_username = "${{ secrets.RDS_USERNAME }}" + rds_password = "${{ secrets.RDS_PASSWORD }}" + domain_name = "${{ secrets.PROD_DOMAIN_NAME }}" + EOF + + - name: Terraform Plan (prod) + id: plan + working-directory: terraform/env/prod + run: | + terraform plan \ + -input=false \ + -no-color \ + -var-file="terraform.secret.tfvars" \ + > plan.txt + + - name: Delete previous Terraform plan comments + uses: actions/github-script@v7 + with: + script: | + const planTag = "## 📝 Terraform Plan Result (Prod)"; + const comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + + for (const comment of comments.data) { + if (comment.body && comment.body.startsWith(planTag)) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }); + } + } + + - name: Comment PR with plan output + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const plan = fs.readFileSync('terraform/env/prod/plan.txt', 'utf8'); + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## 📝 Terraform Plan Result (Prod)\n\n\`\`\`terraform\n${plan}\n\`\`\`` + }); \ No newline at end of file diff --git a/.github/workflows/destroy_all.yml b/.github/workflows/destroy_all.yml new file mode 100644 index 0000000..7d249ae --- /dev/null +++ b/.github/workflows/destroy_all.yml @@ -0,0 +1,205 @@ +name: Terraform Destroy All Environments + +on: + workflow_dispatch: + inputs: + confirm_destroy: + description: '전체 환경 Destroy를 확인하려면 "DESTROY_ALL_ENVIRONMENTS"를 입력하세요' + required: true + type: string + default: '' + reason: + description: '전체 환경 삭제 이유를 입력하세요 (필수)' + required: true + type: string + default: '' + backup_required: + description: 'Production 데이터베이스 백업 상태' + required: true + type: choice + options: + - '백업 완료됨' + - '백업 불필요' + - '백업 진행 중' + environments: + description: '삭제할 환경을 선택하세요' + required: true + type: choice + options: + - 'dev_only' + - 'prod_only' + - 'all_environments' + +jobs: + validate-inputs: + name: Validate Destroy Inputs + runs-on: ubuntu-latest + outputs: + destroy-dev: ${{ steps.validate.outputs.destroy-dev }} + destroy-prod: ${{ steps.validate.outputs.destroy-prod }} + + steps: + - name: Validate destroy confirmation + id: validate + run: | + if [ "${{ github.event.inputs.confirm_destroy }}" != "DESTROY_ALL_ENVIRONMENTS" ]; then + echo "❌ 전체 환경 Destroy 확인이 올바르지 않습니다. 'DESTROY_ALL_ENVIRONMENTS'를 정확히 입력해주세요." + exit 1 + fi + + if [ "${{ github.event.inputs.backup_required }}" = "백업 진행 중" ] && [[ "${{ github.event.inputs.environments }}" == *"prod"* ]]; then + echo "❌ Production 환경이 포함되어 있지만 데이터베이스 백업이 완료되지 않았습니다." + exit 1 + fi + + # 환경별 삭제 여부 결정 + if [ "${{ github.event.inputs.environments }}" = "dev_only" ]; then + echo "destroy-dev=true" >> $GITHUB_OUTPUT + echo "destroy-prod=false" >> $GITHUB_OUTPUT + elif [ "${{ github.event.inputs.environments }}" = "prod_only" ]; then + echo "destroy-dev=false" >> $GITHUB_OUTPUT + echo "destroy-prod=true" >> $GITHUB_OUTPUT + elif [ "${{ github.event.inputs.environments }}" = "all_environments" ]; then + echo "destroy-dev=true" >> $GITHUB_OUTPUT + echo "destroy-prod=true" >> $GITHUB_OUTPUT + fi + + echo "✅ 입력 검증 완료" + echo "📝 Destroy 이유: ${{ github.event.inputs.reason }}" + echo "🎯 대상 환경: ${{ github.event.inputs.environments }}" + echo "💾 백업 상태: ${{ github.event.inputs.backup_required }}" + + destroy-dev: + name: Destroy Development Environment + runs-on: ubuntu-latest + needs: validate-inputs + if: needs.validate-inputs.outputs.destroy-dev == 'true' + + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.12.2 + + - name: Terraform Init (dev) + run: terraform -chdir=terraform/env/dev init + + - name: Create terraform.tfvars for destroy + working-directory: terraform/env/dev + run: | + cat > terraform.tfvars << EOF + aws_access_key_id = "${{ secrets.DEV_AWS_ACCESS_KEY_ID }}" + aws_secret_access_key = "${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}" + rds_username = "${{ secrets.RDS_USERNAME }}" + rds_password = "${{ secrets.RDS_PASSWORD }}" + domain_name = "${{ secrets.DEV_DOMAIN_NAME }}" + user_data = "#!/bin/bash\necho 'Destroy 환경 - 간단한 user_data'" + EOF + + - name: Terraform Destroy (dev) + working-directory: terraform/env/dev + run: | + echo "🗑️ Development 환경을 삭제합니다..." + terraform destroy \ + -auto-approve \ + -input=false \ + -var-file="terraform.tfvars" + + - name: Clean up dev state + run: | + rm -f terraform/env/dev/terraform.tfvars + + destroy-prod: + name: Destroy Production Environment + runs-on: ubuntu-latest + needs: validate-inputs + if: needs.validate-inputs.outputs.destroy-prod == 'true' + + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.12.2 + + - name: Terraform Init (prod) + run: terraform -chdir=terraform/env/prod init + + - name: Create terraform.tfvars for destroy + working-directory: terraform/env/prod + run: | + cat > terraform.tfvars << EOF + aws_access_key_id = "${{ secrets.PROD_AWS_ACCESS_KEY_ID }}" + aws_secret_access_key = "${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }}" + rds_username = "${{ secrets.RDS_USERNAME }}" + rds_password = "${{ secrets.RDS_PASSWORD }}" + domain_name = "${{ secrets.PROD_DOMAIN_NAME }}" + user_data = "#!/bin/bash\necho 'Destroy 환경 - 간단한 user_data'" + EOF + + - name: Final confirmation pause for production + run: | + echo "⚠️ PRODUCTION 환경 삭제를 진행합니다!" + echo "⚠️ 이 작업은 되돌릴 수 없습니다!" + echo "⚠️ 모든 Production 데이터와 서비스가 삭제됩니다!" + echo "" + echo "계속하려면 30초 후 자동으로 진행됩니다..." + sleep 30 + + - name: Terraform Destroy (prod) + working-directory: terraform/env/prod + run: | + echo "🗑️ Production 환경을 삭제합니다..." + terraform destroy \ + -auto-approve \ + -input=false \ + -var-file="terraform.tfvars" + + - name: Clean up prod state + run: | + rm -f terraform/env/prod/terraform.tfvars + + notify-completion: + name: Notify Completion + runs-on: ubuntu-latest + needs: [validate-inputs, destroy-dev, destroy-prod] + if: always() + + steps: + - name: Report completion + run: | + echo "🎯 전체 Destroy 작업 완료!" + echo "📝 Destroy 이유: ${{ github.event.inputs.reason }}" + echo "🎯 대상 환경: ${{ github.event.inputs.environments }}" + echo "💾 백업 상태: ${{ github.event.inputs.backup_required }}" + echo "👤 실행자: ${{ github.actor }}" + echo "🕒 실행 시간: $(date)" + echo "" + echo "📊 작업 결과:" + echo " - Dev 환경: ${{ needs.destroy-dev.result || 'SKIPPED' }}" + echo " - Prod 환경: ${{ needs.destroy-prod.result || 'SKIPPED' }}" diff --git a/.github/workflows/destroy_dev.yml b/.github/workflows/destroy_dev.yml new file mode 100644 index 0000000..2804d04 --- /dev/null +++ b/.github/workflows/destroy_dev.yml @@ -0,0 +1,100 @@ +name: Terraform Destroy Development Environment + +on: + workflow_dispatch: + inputs: + confirm_destroy: + description: 'Destroy를 확인하려면 "DESTROY"를 입력하세요' + required: true + type: string + default: '' + reason: + description: 'Destroy 이유를 입력하세요' + required: true + type: string + default: '' + +jobs: + terraform-destroy: + name: Terraform Destroy Dev Environment + runs-on: ubuntu-latest + + # 보안을 위해 수동 승인이 필요한 환경에서는 이 워크플로우를 비활성화하거나 추가 승인 프로세스를 추가하세요 + + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate destroy confirmation + run: | + if [ "${{ github.event.inputs.confirm_destroy }}" != "DESTROY" ]; then + echo "❌ Destroy 확인이 올바르지 않습니다. 'DESTROY'를 정확히 입력해주세요." + exit 1 + fi + echo "✅ Destroy 확인 완료: ${{ github.event.inputs.reason }}" + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.12.2 + + - name: Terraform Init (dev) + run: terraform -chdir=terraform/env/dev init + + - name: Create terraform.tfvars for destroy + working-directory: terraform/env/dev + run: | + cat > terraform.tfvars << EOF + aws_access_key_id = "${{ secrets.DEV_AWS_ACCESS_KEY_ID }}" + aws_secret_access_key = "${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}" + rds_username = "${{ secrets.RDS_USERNAME }}" + rds_password = "${{ secrets.RDS_PASSWORD }}" + domain_name = "${{ secrets.DEV_DOMAIN_NAME }}" + user_data = "#!/bin/bash\necho 'Destroy 환경 - 간단한 user_data'" + EOF + + - name: Terraform Plan Destroy (dev) + working-directory: terraform/env/dev + run: | + echo "🔍 Destroy할 리소스를 확인합니다..." + terraform plan -destroy \ + -input=false \ + -no-color \ + -var-file="terraform.tfvars" \ + > destroy_plan.txt + + echo "📋 Destroy 계획:" + cat destroy_plan.txt + + - name: Terraform Destroy (dev) + working-directory: terraform/env/dev + run: | + echo "⚠️ Dev 환경의 모든 리소스를 삭제합니다..." + echo "이 작업은 되돌릴 수 없습니다!" + terraform destroy \ + -auto-approve \ + -input=false \ + -var-file="terraform.tfvars" + + - name: Clean up local state + run: | + echo "🧹 로컬 상태 파일을 정리합니다..." + rm -f terraform/env/dev/terraform.tfvars + rm -f terraform/env/dev/destroy_plan.txt + + - name: Notify completion + run: | + echo "✅ Dev 환경 Destroy 완료!" + echo "📝 Destroy 이유: ${{ github.event.inputs.reason }}" + echo "👤 실행자: ${{ github.actor }}" + echo "🕒 실행 시간: $(date)" diff --git a/.github/workflows/destroy_prod.yml b/.github/workflows/destroy_prod.yml new file mode 100644 index 0000000..5c2a990 --- /dev/null +++ b/.github/workflows/destroy_prod.yml @@ -0,0 +1,126 @@ +name: Terraform Destroy Production Environment + +on: + workflow_dispatch: + inputs: + confirm_destroy: + description: 'Production Destroy를 확인하려면 "DESTROY_PRODUCTION"를 입력하세요' + required: true + type: string + default: '' + reason: + description: 'Destroy 이유를 입력하세요 (필수)' + required: true + type: string + default: '' + backup_required: + description: '데이터베이스 백업이 필요한지 확인하세요' + required: true + type: choice + options: + - '백업 완료됨' + - '백업 불필요' + - '백업 진행 중' + +jobs: + terraform-destroy: + name: Terraform Destroy Production Environment + runs-on: ubuntu-latest + + # Production 환경은 매우 신중하게 처리해야 합니다 + # 필요시 추가 승인 프로세스를 구현하세요 + + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate destroy confirmation and backup + run: | + if [ "${{ github.event.inputs.confirm_destroy }}" != "DESTROY_PRODUCTION" ]; then + echo "❌ Production Destroy 확인이 올바르지 않습니다. 'DESTROY_PRODUCTION'를 정확히 입력해주세요." + exit 1 + fi + + if [ "${{ github.event.inputs.backup_required }}" = "백업 진행 중" ]; then + echo "❌ 데이터베이스 백업이 완료되지 않았습니다. 백업을 먼저 완료해주세요." + exit 1 + fi + + echo "✅ Production Destroy 확인 완료" + echo "📝 Destroy 이유: ${{ github.event.inputs.reason }}" + echo "💾 백업 상태: ${{ github.event.inputs.backup_required }}" + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.12.2 + + - name: Terraform Init (prod) + run: terraform -chdir=terraform/env/prod init + + - name: Create terraform.tfvars for destroy + working-directory: terraform/env/prod + run: | + cat > terraform.tfvars << EOF + aws_access_key_id = "${{ secrets.PROD_AWS_ACCESS_KEY_ID }}" + aws_secret_access_key = "${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }}" + rds_username = "${{ secrets.RDS_USERNAME }}" + rds_password = "${{ secrets.RDS_PASSWORD }}" + domain_name = "${{ secrets.PROD_DOMAIN_NAME }}" + user_data = "#!/bin/bash\necho 'Destroy 환경 - 간단한 user_data'" + EOF + + - name: Terraform Plan Destroy (prod) + working-directory: terraform/env/prod + run: | + echo "🔍 Production 환경에서 Destroy할 리소스를 확인합니다..." + terraform plan -destroy \ + -input=false \ + -no-color \ + -var-file="terraform.tfvars" \ + > destroy_plan.txt + + echo "📋 Production Destroy 계획:" + cat destroy_plan.txt + + - name: Final confirmation pause + run: | + echo "⚠️ PRODUCTION 환경 삭제를 진행합니다!" + echo "⚠️ 이 작업은 되돌릴 수 없습니다!" + echo "⚠️ 모든 Production 데이터와 서비스가 삭제됩니다!" + echo "" + echo "계속하려면 30초 후 자동으로 진행됩니다..." + sleep 30 + + - name: Terraform Destroy (prod) + working-directory: terraform/env/prod + run: | + echo "🗑️ Production 환경의 모든 리소스를 삭제합니다..." + terraform destroy \ + -auto-approve \ + -input=false \ + -var-file="terraform.tfvars" + + - name: Clean up local state + run: | + echo "🧹 로컬 상태 파일을 정리합니다..." + rm -f terraform/env/prod/terraform.tfvars + rm -f terraform/env/prod/destroy_plan.txt + + - name: Notify completion + run: | + echo "✅ Production 환경 Destroy 완료!" + echo "📝 Destroy 이유: ${{ github.event.inputs.reason }}" + echo "💾 백업 상태: ${{ github.event.inputs.backup_required }}" + echo "👤 실행자: ${{ github.actor }}" + echo "🕒 실행 시간: $(date)" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d2fc2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Default ignored files +/.idea/ + +# Terraform state files (local state) +terraform.tfstate +terraform.tfstate.backup +.terraform.lock.hcl + +# Terraform override files +*.tfvars.json +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Terraform CLI configuration files +.terraformrc +terraform.rc + +# Terraform directories +.terraform/ +.terraform.lock.hcl + +# AWS credentials and config +.aws/ +aws.credentials + +# Secret tfvars files +*.secret.tfvars +secret.tfvars \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..37695fc --- /dev/null +++ b/README.md @@ -0,0 +1,230 @@ +# Clokey Infrastructure as Code (Terraform) + +AWS 인프라를 Terraform으로 관리하는 Infrastructure as Code 프로젝트입니다. CI/CD 파이프라인이 구축되어 있어 GitHub Actions를 통해 자동화된 배포가 가능합니다. + +## 🏗️ 프로젝트 구조 + +``` +clokey-iac/ +├── .github/ +│ └── workflows/ +│ ├── ci_dev.yml # Dev 환경 CI +│ ├── ci_prod.yml # Prod 환경 CI +│ ├── cd_dev.yml # Dev 환경 CD +│ └── cd_prod.yml # Prod 환경 CD +├── terraform/ +│ ├── bootstrap/ # Bootstrap 모듈 (S3 백엔드 생성) +│ │ ├── terraform.tf +│ │ ├── provider.tf +│ │ ├── variables.tf +│ │ ├── tf_state_bucket.tf +│ │ ├── outputs.tf +│ │ ├── terraform.tfvars +│ │ └── example.tfvars +│ ├── modules/ # 재사용 가능한 모듈들 +│ │ ├── compute/ec2/ +│ │ ├── database/rds/ +│ │ ├── network/ +│ │ ├── security/ +│ │ └── storage/s3/ +│ └── env/ # 환경별 설정 +│ ├── dev/ +│ └── prod/ +└── README.md +``` + +## 🔧 구성 요소 + +### Bootstrap +- **S3 Bucket**: Terraform 상태 파일 저장 +- **버전 관리**: 상태 파일 변경 이력 추적 +- **암호화**: AES256 서버 사이드 암호화 +- **보안**: 공개 액세스 차단 + +### Dev/Prod 환경 +- **VPC**: 가상 프라이빗 클라우드 +- **Subnets**: 퍼블릭/프라이빗 서브넷 (2개 AZ) +- **EC2**: 웹 애플리케이션 서버 +- **RDS**: MySQL 데이터베이스 +- **S3**: 파일 저장소 +- **Route53**: DNS 관리 (EC2 Public IP 자동 연결) +- **Security Groups**: 방화벽 규칙 + +## 🚀 시작하기 + +### 1. 사전 준비 + +#### AWS 인증 설정 +```bash +# AWS CLI 설정 +aws configure +# AWS Access Key ID: [your-access-key] +# AWS Secret Access Key: [your-secret-key] +# Default region name: ap-northeast-2 +# Default output format: json + +# 또는 환경 변수 설정 +export AWS_ACCESS_KEY_ID="your-access-key" +export AWS_SECRET_ACCESS_KEY="your-secret-key" +export AWS_DEFAULT_REGION="ap-northeast-2" +``` + +### 2. Bootstrap 실행 (최초 1회) + +```bash +cd terraform/bootstrap + +# secret.tfvars 파일 생성 +cat > secret.tfvars << EOF +access_key_id = "YOUR_ACCESS_KEY_ID" +secret_access_key = "YOUR_SECRET_ACCESS_KEY" +EOF + +# Bootstrap 실행 +terraform init +terraform plan -var-file="terraform.tfvars" -var-file="secret.tfvars" +terraform apply -var-file="terraform.tfvars" -var-file="secret.tfvars" + +# S3 버킷명 확인 +terraform output state_bucket_name +``` + +### 3. 환경별 설정 + +#### Dev 환경 설정 +```bash +cd terraform/env/dev + +# terraform.tfvars 파일 편집 +# - 실제 도메인 설정 +# - RDS 사용자명 설정 + +# Dev 환경 실행 +terraform init +terraform plan -var-file="terraform.tfvars" +terraform apply -var-file="terraform.tfvars" +``` + +#### Prod 환경 설정 +```bash +cd terraform/env/prod + +# terraform.tfvars 파일 편집 +# - 실제 도메인 설정 +# - RDS 사용자명 설정 + +# Prod 환경 실행 +terraform init +terraform plan -var-file="terraform.tfvars" +terraform apply -var-file="terraform.tfvars" +``` + +## 🔄 CI/CD 파이프라인 + +### GitHub Actions 설정 + +#### GitHub Secrets 설정 +다음 값들을 GitHub 저장소의 Secrets에 설정하세요: + +| Secret Name | Description | +|-------------|-------------| +| `DEV_AWS_ACCESS_KEY_ID` | Dev 환경 AWS Access Key | +| `DEV_AWS_SECRET_ACCESS_KEY` | Dev 환경 AWS Secret Key | +| `PROD_AWS_ACCESS_KEY_ID` | Prod 환경 AWS Access Key | +| `PROD_AWS_SECRET_ACCESS_KEY` | Prod 환경 AWS Secret Key | + +#### 자동 배포 +- **Dev 브랜치**: `dev` 브랜치에 push 시 Dev 환경 자동 배포 +- **Prod 환경**: `prod` 브랜치에 push 시 Prod 환경 자동 배포 + +## 🔒 보안 고려사항 + +### 파일 보안 +- `example.tfvars` Git에 커밋되어 템플릿으로 사용됩니다 +- `*.secret.tfvars` 파일은 민감한 정보를 포함하며 Git에서 제외됩니다 + +### AWS 보안 +- AWS 인증 정보는 GitHub Secrets로 관리 +- 최소 권한 원칙 적용 +- 정기적인 키 로테이션 권장 + +### Terraform 상태 관리 +- S3 백엔드로 중앙화된 상태 관리 +- 상태 파일 암호화 +- 버전 관리로 이전 상태 복구 가능 + +## 📝 파일 설명 + +### Bootstrap +- `terraform.tf`: Terraform 버전 및 provider 요구사항 +- `provider.tf`: AWS Provider 설정 +- `variables.tf`: 입력 변수 정의 +- `tf_state_bucket.tf`: S3 백엔드 버킷 생성 +- `outputs.tf`: 출력 값 정의 +- `terraform.tfvars`: 변수 값 (Git에서 제외) +- `example.tfvars`: 변수 템플릿 (Git에 포함) + +### 환경별 설정 +- `backend.tf`: S3 백엔드 설정 +- `provider.tf`: AWS Provider 설정 +- `locals.tf`: 공통 변수 정의 +- `variables.tf`: 입력 변수 정의 +- `data.tf`: 데이터 소스 정의 +- `network.tf`: 네트워크 인프라 +- `compute.tf`: 컴퓨팅 리소스 +- `database.tf`: 데이터베이스 리소스 +- `storage.tf`: 스토리지 리소스 +- `outputs.tf`: 출력 값 정의 +- `terraform.tfvars`: 환경별 변수 값 (Git에서 제외) +- `example.tfvars`: 변수 템플릿 (Git에 포함) + +## 🛠️ 모듈 구조 + +### Compute +- **EC2**: 웹 서버 인스턴스 + - Amazon Linux 2023 AMI + - t3.micro 인스턴스 타입 + - 퍼블릭 서브넷에 배치 + +### Database +- **RDS**: MySQL 데이터베이스 + - MySQL 8.0 + - 프라이빗 서브넷에 배치 + - AWS Secrets Manager로 비밀번호 관리 + +### Network +- **VPC**: 10.0.0.0/16 CIDR +- **Subnets**: 2개 AZ (ap-northeast-2a, ap-northeast-2c) + - 퍼블릭 서브넷: 10.0.1.0/24, 10.0.2.0/24 + - 프라이빗 서브넷: 10.0.11.0/24, 10.0.12.0/24 +- **Route53**: DNS 관리 및 A 레코드 자동 설정 + +### Security +- **Security Groups**: EC2, RDS, ALB용 보안 그룹 +- **NACLs**: 서브넷 레벨 네트워크 제어 + +### Storage +- **S3**: 파일 저장소 + - 버전 관리 활성화 + - 암호화 설정 + - 공개 액세스 차단 + +## 🚨 주의사항 + +### 비용 관리 +- 프로덕션 환경에 적용하기 전에 비용을 확인하세요 +- 적절한 인스턴스 타입을 선택하세요 +- 사용하지 않는 리소스는 정기적으로 정리하세요 + +### 보안 +- 도메인 정보는 절대 GitHub에 커밋하지 마세요 +- AWS 키는 정기적으로 로테이션하세요 +- 프로덕션 환경에서는 더 강력한 보안 설정을 적용하세요 + +### 백업 +- 정기적으로 Terraform 상태를 백업하세요 +- 중요한 데이터는 별도로 백업하세요 + +## 📞 지원 + +문제가 발생하거나 질문이 있으시면 이슈를 생성해주세요. \ No newline at end of file diff --git a/terraform/bootstrap/example.tfvars b/terraform/bootstrap/example.tfvars new file mode 100644 index 0000000..16229bd --- /dev/null +++ b/terraform/bootstrap/example.tfvars @@ -0,0 +1,9 @@ +# AWS Region +aws_region = "ap-northeast-2" + +# Environment (dev, prod) +environment = "dev" + +# AWS Authentication (실제 값으로 변경 필요) +# access_key_id = "YOUR_ACCESS_KEY_ID" +# secret_access_key = "YOUR_SECRET_ACCESS_KEY" diff --git a/terraform/bootstrap/outputs.tf b/terraform/bootstrap/outputs.tf new file mode 100644 index 0000000..4a753e9 --- /dev/null +++ b/terraform/bootstrap/outputs.tf @@ -0,0 +1,9 @@ +output "state_bucket_name" { + description = "Name of the S3 bucket for Terraform state" + value = module.tf_state_bucket.bucket_name +} + +output "state_bucket_arn" { + description = "ARN of the S3 bucket for Terraform state" + value = module.tf_state_bucket.bucket_arn +} diff --git a/terraform/bootstrap/provider.tf b/terraform/bootstrap/provider.tf new file mode 100644 index 0000000..b1cfc54 --- /dev/null +++ b/terraform/bootstrap/provider.tf @@ -0,0 +1,5 @@ +provider "aws" { + region = var.aws_region + access_key = var.access_key_id + secret_key = var.secret_access_key +} diff --git a/terraform/bootstrap/terraform.tf b/terraform/bootstrap/terraform.tf new file mode 100644 index 0000000..a8de733 --- /dev/null +++ b/terraform/bootstrap/terraform.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/terraform/bootstrap/terraform.tfvars b/terraform/bootstrap/terraform.tfvars new file mode 100644 index 0000000..3e8dbe7 --- /dev/null +++ b/terraform/bootstrap/terraform.tfvars @@ -0,0 +1,2 @@ +aws_region = "ap-northeast-2" +environment = "dev" diff --git a/terraform/bootstrap/tf_state_bucket.tf b/terraform/bootstrap/tf_state_bucket.tf new file mode 100644 index 0000000..2cee9b6 --- /dev/null +++ b/terraform/bootstrap/tf_state_bucket.tf @@ -0,0 +1,24 @@ +# 현재 AWS 계정 정보 +data "aws_caller_identity" "current" {} + +# Terraform 상태 파일을 저장할 S3 버킷 생성 +module "tf_state_bucket" { + source = "../modules/storage/s3" + + bucket_name = "clokey-terraform-state-${var.environment}" + environment = var.environment + purpose = "tfstate" + + # 보안 설정 + enable_versioning = true + enable_sse = true + sse_algorithm = "AES256" + enable_block_public_access = true + + # 추가 태그 + tags = { + Name = "Terraform State Bucket" + Environment = var.environment + } +} + diff --git a/terraform/bootstrap/variables.tf b/terraform/bootstrap/variables.tf new file mode 100644 index 0000000..e3c3201 --- /dev/null +++ b/terraform/bootstrap/variables.tf @@ -0,0 +1,27 @@ +variable "aws_region" { + description = "AWS region" + type = string + default = "ap-northeast-2" +} + +variable "environment" { + description = "Environment name (dev, prod, etc.)" + type = string + default = "dev" + validation { + condition = contains(["dev", "prod"], var.environment) + error_message = "Environment must be one of: dev, prod." + } +} + +variable "access_key_id" { + description = "AWS Access Key ID" + type = string + sensitive = true +} + +variable "secret_access_key" { + description = "AWS Secret Access Key" + type = string + sensitive = true +} diff --git a/terraform/env/dev/backend.tf b/terraform/env/dev/backend.tf new file mode 100644 index 0000000..fd02b23 --- /dev/null +++ b/terraform/env/dev/backend.tf @@ -0,0 +1,9 @@ +# S3 Backend Configuration +terraform { + backend "s3" { + bucket = "clokey-terraform-state-dev" + key = "dev/terraform.tfstate" + region = "ap-northeast-2" + encrypt = true + } +} diff --git a/terraform/env/dev/compute.tf b/terraform/env/dev/compute.tf new file mode 100644 index 0000000..2f49204 --- /dev/null +++ b/terraform/env/dev/compute.tf @@ -0,0 +1,36 @@ +# EC2 Instance +module "ec2" { + source = "../../modules/compute/ec2" + ami = data.aws_ami.ubuntu_latest.id + instance_type = "t3.micro" + subnet_id = module.subnet_public_a.subnet_id + name = "${local.name_prefix}-api" #API 서버용 + security_group_id_list = [module.sg_ec2.security_group_id] + environment = local.environment + purpose = "was" + + # SSH 키 설정 (AWS 콘솔에서 미리 생성한 키 페어 사용) + key_name = "clokey-dev-server-key" + + # 퍼블릭 IP 활성화 (웹 서버용) + associate_public_ip_address = true + + # 루트 볼륨 설정 + root_volume_size = 30 + root_volume_type = "gp3" + root_volume_encrypted = true + + # 종료 시 중지 (삭제하지 않음) + instance_initiated_shutdown_behavior = "stop" + + # 사용자 데이터 (locals에서 로드된 base64 인코딩된 스크립트) + user_data = local.user_data_base64 +} + +# ALB Target Group 추가 +resource "aws_lb_target_group_attachment" "ec2" { + target_group_arn = module.alb.target_group_arn + target_id = module.ec2.instance_id + port = 80 +} + diff --git a/terraform/env/dev/data.tf b/terraform/env/dev/data.tf new file mode 100644 index 0000000..464696c --- /dev/null +++ b/terraform/env/dev/data.tf @@ -0,0 +1,26 @@ +# 가용영역 데이터 +data "aws_availability_zones" "available" { + state = "available" +} + +# 최신 Ubuntu 22.04 LTS (Jammy) AMI +data "aws_ami" "ubuntu_latest" { + most_recent = true + owners = ["099720109477"] + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +# 현재 AWS 계정 정보 +data "aws_caller_identity" "current" {} + +# 현재 리전 정보 +data "aws_region" "current" {} diff --git a/terraform/env/dev/database.tf b/terraform/env/dev/database.tf new file mode 100644 index 0000000..f94025d --- /dev/null +++ b/terraform/env/dev/database.tf @@ -0,0 +1,49 @@ +# RDS Instance +module "rds" { + source = "../../modules/database/rds" + name = "${local.name_prefix}-rds" + subnet_ids = [ + module.subnet_private_a.subnet_id, + module.subnet_private_c.subnet_id + ] + storage = 30 + engine = "mysql" + engine_version = "8.0.42" + instance_class = "db.t3.micro" + db_name = "clokey_db" + username = var.rds_username + password = var.rds_password + security_group_id = module.sg_rds.security_group_id + environment = local.environment + purpose = "app" + + # 네트워크 설정 + publicly_accessible = false # 프라이빗 서브넷에 위치하므로 false + + # 백업 설정 + backup_retention_period = 7 + backup_window = "03:00-04:00" + maintenance_window = "sun:04:00-sun:05:00" + + # 성능 설정 + multi_az = false # 개발 환경에서는 단일 AZ + storage_type = "gp3" + storage_encrypted = true + + # 보안 설정 + deletion_protection = false # 개발 환경에서는 삭제 보호 비활성화 + + # 파라미터 그룹 설정 + parameter_group_family = "mysql8.0" + parameter_group_parameters = [ + { + name = "max_connections" + value = "100" + }, + { + name = "innodb_buffer_pool_size" + value = "{DBInstanceClassMemory*3/4}" + } + ] +} + diff --git a/terraform/env/dev/example.tfvars b/terraform/env/dev/example.tfvars new file mode 100644 index 0000000..ceb8fb4 --- /dev/null +++ b/terraform/env/dev/example.tfvars @@ -0,0 +1,17 @@ +# AWS Region +aws_region = "ap-northeast-2" + +# Environment +environment = "dev" + +# VPC Configuration +vpc_cidr_block = "10.0.0.0/16" +public_subnet_cidr = "10.0.1.0/24" +availability_zone = "ap-northeast-2a" + +# RDS Configuration +rds_username = "admin" # 실제 환경에서는 더 복잡한 비밀번호 사용 + +# Route53 Configuration +# hosted_zone_id = "YOUR_HOSTED_ZONE_ID" # 도메인의 hosted zone ID +# domain_name = "yourdomain.com" # 실제 도메인으로 변경 diff --git a/terraform/env/dev/locals.tf b/terraform/env/dev/locals.tf new file mode 100644 index 0000000..0f88f15 --- /dev/null +++ b/terraform/env/dev/locals.tf @@ -0,0 +1,23 @@ +locals { + # 환경 설정 + environment = "dev" + aws_region = "ap-northeast-2" + + # 공통 태그 + common_tags = { + Environment = local.environment + Project = "clokey" + ManagedBy = "terraform" + } + + # 이름 규칙 + name_prefix = "${local.environment}-clokey" + + # Backend 설정 (하드코딩) + state_bucket_name = "clokey-terraform-state-116541188992" + state_key = "dev/terraform.tfstate" + + # UserData 설정 (파일에서 base64로 인코딩하여 로드) + user_data_base64 = filebase64("${path.module}/../../../userdata-examples/was-userdata.sh") +} + diff --git a/terraform/env/dev/network.tf b/terraform/env/dev/network.tf new file mode 100644 index 0000000..73dcbba --- /dev/null +++ b/terraform/env/dev/network.tf @@ -0,0 +1,265 @@ +# VPC +module "vpc" { + source = "../../modules/network/vpc" + cidr_block = "10.0.0.0/16" + name = "${local.name_prefix}-vpc" + purpose = "main" + environment = local.environment + tags = local.common_tags +} + +# Internet Gateway +module "igw" { + source = "../../modules/network/igw" + vpc_id = module.vpc.vpc_id + name = "${local.name_prefix}-igw" + purpose = "main" +} + +# Public Route Table +module "route_table_public" { + source = "../../modules/network/route_table" + vpc_id = module.vpc.vpc_id + gateway_id = module.igw.gateway_id + enable_igw_route = true + name = "${local.name_prefix}-public-rt" + access_level = "public" + purpose = "public" +} + +# Private Route Table +module "route_table_private" { + source = "../../modules/network/route_table" + vpc_id = module.vpc.vpc_id + enable_igw_route = false + name = "${local.name_prefix}-private-rt" + access_level = "private" + purpose = "private" +} + +# Public Subnets +module "subnet_public_a" { + source = "../../modules/network/subnet" + vpc_id = module.vpc.vpc_id + cidr_block = "10.0.1.0/24" + az = "ap-northeast-2a" + map_public_ip = true + name = "${local.name_prefix}-subnet-public-a" + route_table_id = module.route_table_public.route_table_id + environment = local.environment + purpose = "public" +} + +module "subnet_public_c" { + source = "../../modules/network/subnet" + vpc_id = module.vpc.vpc_id + cidr_block = "10.0.2.0/24" + az = "ap-northeast-2c" + map_public_ip = true + name = "${local.name_prefix}-subnet-public-c" + route_table_id = module.route_table_public.route_table_id + environment = local.environment + purpose = "public" +} + +# Private Subnets +module "subnet_private_a" { + source = "../../modules/network/subnet" + vpc_id = module.vpc.vpc_id + cidr_block = "10.0.11.0/24" + az = "ap-northeast-2a" + map_public_ip = false + name = "${local.name_prefix}-subnet-private-a" + route_table_id = module.route_table_private.route_table_id + environment = local.environment + purpose = "private" +} + +module "subnet_private_c" { + source = "../../modules/network/subnet" + vpc_id = module.vpc.vpc_id + cidr_block = "10.0.12.0/24" + az = "ap-northeast-2c" + map_public_ip = false + name = "${local.name_prefix}-subnet-private-c" + route_table_id = module.route_table_private.route_table_id + environment = local.environment + purpose = "private" +} + +# EC2 Security Group +module "sg_ec2" { + source = "../../modules/security/security_group" + vpc_id = module.vpc.vpc_id + + environment = local.environment + purpose = "ec2" + security_group_name = "${local.name_prefix}-sg-ec2" + + ingress_rules = [ + { + from_port = 80 + to_port = 80 + protocol = "tcp" + use_cidr = false + use_sg = true + source_security_group_id = module.sg_alb.security_group_id + }, + { + from_port = 22 + to_port = 22 + protocol = "tcp" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] + + egress_rules = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] +} + +# RDS Security Group +module "sg_rds" { + source = "../../modules/security/security_group" + vpc_id = module.vpc.vpc_id + + environment = local.environment + purpose = "rds" + security_group_name = "${local.name_prefix}-sg-rds" + + ingress_rules = [ + { + from_port = 3306 + to_port = 3306 + protocol = "tcp" + use_cidr = false + use_sg = true + source_security_group_id = module.sg_ec2.security_group_id + } + ] + + egress_rules = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] +} + +# ALB Security Group +module "sg_alb" { + source = "../../modules/security/security_group" + vpc_id = module.vpc.vpc_id + + environment = local.environment + purpose = "alb" + security_group_name = "${local.name_prefix}-sg-alb" + + ingress_rules = [ + { + from_port = 80 + to_port = 80 + protocol = "tcp" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + }, + { + from_port = 443 + to_port = 443 + protocol = "tcp" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] + + egress_rules = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] +} + +# ACM Certificate (1단계: 인증서만 생성, 검증은 나중에) +module "acm" { + source = "../../modules/network/acm" + + name_prefix = local.name_prefix + domain_name = var.domain_name + hosted_zone_id = module.route53_zone.hosted_zone_id + create_validation = false # 1단계에서는 검증 비활성화 + + tags = local.common_tags +} + +# Application Load Balancer +module "alb" { + source = "../../modules/network/alb" + + name_prefix = local.name_prefix + internal = false + security_groups = [module.sg_alb.security_group_id] + subnet_ids = [module.subnet_public_a.subnet_id, module.subnet_public_c.subnet_id] + vpc_id = module.vpc.vpc_id + + target_group_port = 80 + target_group_protocol = "HTTP" + + health_check_path = "/health" + health_check_matcher = "200" + + # HTTPS 리스너 비활성화 (인증서 검증 완료 후 활성화) + create_https_listener = false + certificate_arn = null + + tags = local.common_tags +} + +# Route53 - Hosted Zone 생성 +module "route53_zone" { + source = "../../modules/network/route53" + + # 새로운 hosted zone 생성 + create_hosted_zone = true + domain_name = var.domain_name + create_a_record = false + + tags = local.common_tags +} + +# Route53 - ALB를 A 레코드로 설정 (ALB 생성 후) +module "route53_record" { + source = "../../modules/network/route53" + + # 기존 hosted zone 사용 + create_hosted_zone = false + hosted_zone_id = module.route53_zone.hosted_zone_id + + # A 레코드 생성 (ALB로 변경) + create_a_record = true + record_name = "${local.environment}.${var.domain_name}" + target_alias = module.alb.load_balancer_dns_name + target_zone_id = module.alb.load_balancer_zone_id + ttl = 300 + + depends_on = [module.alb] +} diff --git a/terraform/env/dev/provider.tf b/terraform/env/dev/provider.tf new file mode 100644 index 0000000..c3e7d27 --- /dev/null +++ b/terraform/env/dev/provider.tf @@ -0,0 +1,9 @@ +provider "aws" { + region = local.aws_region + + # AWS 자격증명은 환경변수(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)에서 자동으로 가져옴 + + default_tags { + tags = local.common_tags + } +} diff --git a/terraform/env/dev/storage.tf b/terraform/env/dev/storage.tf new file mode 100644 index 0000000..86a75d5 --- /dev/null +++ b/terraform/env/dev/storage.tf @@ -0,0 +1,17 @@ +# S3 Bucket +module "s3" { + source = "../../modules/storage/s3" + bucket_name = "dev-clokey-storage-bucket" + environment = local.environment + purpose = "storage" + + # Private bucket (보안 강화) + enable_public_read = false + enable_block_public_access = true + enable_versioning = true + enable_sse = true + sse_algorithm = "AES256" + + tags = local.common_tags +} + diff --git a/terraform/env/dev/terraform.tf b/terraform/env/dev/terraform.tf new file mode 100644 index 0000000..60997a7 --- /dev/null +++ b/terraform/env/dev/terraform.tf @@ -0,0 +1,11 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + diff --git a/terraform/env/dev/terraform.tfvars b/terraform/env/dev/terraform.tfvars new file mode 100644 index 0000000..5720a2b --- /dev/null +++ b/terraform/env/dev/terraform.tfvars @@ -0,0 +1,14 @@ +aws_region = "ap-northeast-2" +environment = "dev" +vpc_cidr_block = "10.0.0.0/16" +public_subnet_cidr = "10.0.1.0/24" +availability_zone = "ap-northeast-2a" + +# RDS 설정 (기본값 - 민감한 정보는 secret.tfvars에서 관리) +# rds_username은 secret.tfvars에서 관리 + +# Route53 설정 (기본값 - 민감한 정보는 secret.tfvars에서 관리) +# domain_name은 secret.tfvars에서 관리 + +# User Data (기본값 - 민감한 정보는 secret.tfvars에서 관리) +# userdata는 locals.tf에서 filebase64() 함수로 로드됨 diff --git a/terraform/env/dev/variables.tf b/terraform/env/dev/variables.tf new file mode 100644 index 0000000..0bb9ff5 --- /dev/null +++ b/terraform/env/dev/variables.tf @@ -0,0 +1,32 @@ +# 민감한 정보만 변수화 (CI/CD에서 관리) +variable "aws_access_key_id" { + description = "AWS Access Key ID" + type = string + sensitive = true +} + +variable "aws_secret_access_key" { + description = "AWS Secret Access Key" + type = string + sensitive = true +} + +variable "rds_username" { + description = "Username for RDS database" + type = string + sensitive = true +} + +variable "rds_password" { + description = "Password for RDS database" + type = string + sensitive = true +} + +# Route53 설정 (도메인 관련) +variable "domain_name" { + description = "Base domain name for Route53 records" + type = string + default = "example.com" + sensitive = true +} diff --git a/terraform/env/prod/backend.tf b/terraform/env/prod/backend.tf new file mode 100644 index 0000000..f3a0f8b --- /dev/null +++ b/terraform/env/prod/backend.tf @@ -0,0 +1,14 @@ +# S3 + DynamoDB Backend Configuration +# 주석을 해제하고 실제 값으로 변경하여 사용 +/* +terraform { + backend "s3" { + bucket = "clokey-terraform-state" + key = "prod/terraform.tfstate" + region = "ap-northeast-2" + dynamodb_table = "clokey-terraform-locks" + encrypt = true + } +} +*/ + diff --git a/terraform/env/prod/compute.tf b/terraform/env/prod/compute.tf new file mode 100644 index 0000000..70538f3 --- /dev/null +++ b/terraform/env/prod/compute.tf @@ -0,0 +1,39 @@ +# EC2 Instance +module "ec2" { + source = "../../modules/compute/ec2" + ami = data.aws_ami.ubuntu_latest.id + instance_type = "t3.micro" + subnet_id = module.subnet_public_a.subnet_id + name = "${local.name_prefix}-api" + security_group_id_list = [module.sg_ec2.security_group_id] + environment = local.environment + purpose = "was" + + # SSH 키 설정 (AWS에서 미리 생성한 키 페어 이름) + key_name = "clokey-prod-server-key" + + # 퍼블릭 IP 활성화 (웹 서버용) + associate_public_ip_address = true + + # 루트 볼륨 설정 + root_volume_size = 30 + root_volume_type = "gp3" + root_volume_encrypted = true + + # 종료 보호 활성화 (프로덕션 환경) + disable_api_termination = true + + # 종료 시 중지 (삭제하지 않음) + instance_initiated_shutdown_behavior = "stop" + + # 사용자 데이터 (locals에서 로드된 base64 인코딩된 스크립트) + user_data = local.user_data_base64 +} + +# ALB Target Group 추가 +resource "aws_lb_target_group_attachment" "ec2" { + target_group_arn = module.alb.target_group_arn + target_id = module.ec2.instance_id + port = 80 +} + diff --git a/terraform/env/prod/data.tf b/terraform/env/prod/data.tf new file mode 100644 index 0000000..f3921a3 --- /dev/null +++ b/terraform/env/prod/data.tf @@ -0,0 +1,27 @@ +# 가용영역 데이터 +data "aws_availability_zones" "available" { + state = "available" +} + +# 최신 Ubuntu 22.04 LTS (Jammy) AMI +data "aws_ami" "ubuntu_latest" { + most_recent = true + owners = ["099720109477"] + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +# 현재 AWS 계정 정보 +data "aws_caller_identity" "current" {} + +# 현재 리전 정보 +data "aws_region" "current" {} + diff --git a/terraform/env/prod/database.tf b/terraform/env/prod/database.tf new file mode 100644 index 0000000..d2e0100 --- /dev/null +++ b/terraform/env/prod/database.tf @@ -0,0 +1,49 @@ +# RDS Instance +module "rds" { + source = "../../modules/database/rds" + name = "${local.name_prefix}-rds" + subnet_ids = [ + module.subnet_private_a.subnet_id, + module.subnet_private_c.subnet_id + ] + storage = 50 + engine = "mysql" + engine_version = "8.0.42" + instance_class = "db.t3.small" + db_name = "clokey_db" + username = var.rds_username + password = var.rds_password + security_group_id = module.sg_rds.security_group_id + environment = local.environment + purpose = "app" + + # 네트워크 설정 + publicly_accessible = false # 프라이빗 서브넷에 위치하므로 false + + # 백업 설정 + backup_retention_period = 30 # 프로덕션에서는 30일 보관 + backup_window = "02:00-03:00" + maintenance_window = "sun:02:00-sun:03:00" + + # 성능 설정 + multi_az = true # 프로덕션에서는 Multi-AZ 활성화 + storage_type = "gp3" + storage_encrypted = true + + # 보안 설정 + deletion_protection = true # 프로덕션에서는 삭제 보호 활성화 + + # 파라미터 그룹 설정 (선택적) + parameter_group_family = "mysql8.0" + parameter_group_parameters = [ + { + name = "max_connections" + value = "200" + }, + { + name = "innodb_buffer_pool_size" + value = "{DBInstanceClassMemory*3/4}" + } + ] +} + diff --git a/terraform/env/prod/example.tfvars b/terraform/env/prod/example.tfvars new file mode 100644 index 0000000..6e6f75c --- /dev/null +++ b/terraform/env/prod/example.tfvars @@ -0,0 +1,18 @@ +# AWS Region +aws_region = "ap-northeast-2" + +# Environment +environment = "prod" + +# VPC Configuration +vpc_cidr_block = "10.0.0.0/16" +public_subnet_cidr = "10.0.1.0/24" +availability_zone = "ap-northeast-2a" + +# RDS Configuration +rds_username = "admin" # 실제 환경에서는 더 복잡한 비밀번호 사용 + +# Route53 Configuration +# hosted_zone_id = "YOUR_HOSTED_ZONE_ID" # 도메인의 hosted zone ID +# domain_name = "yourdomain.com" # 실제 도메인으로 변경 + diff --git a/terraform/env/prod/locals.tf b/terraform/env/prod/locals.tf new file mode 100644 index 0000000..fba7aaf --- /dev/null +++ b/terraform/env/prod/locals.tf @@ -0,0 +1,23 @@ +locals { + # 환경 설정 + environment = "prod" + aws_region = "ap-northeast-2" + + # 공통 태그 + common_tags = { + Environment = local.environment + Project = "clokey" + ManagedBy = "terraform" + } + + # 이름 규칙 + name_prefix = "${local.environment}-clokey" + + # Backend 설정 (하드코딩) + state_bucket_name = "clokey-terraform-state-116541188992" + state_key = "prod/terraform.tfstate" + + # UserData 설정 (파일에서 base64로 인코딩하여 로드) + user_data_base64 = filebase64("${path.module}/../../../userdata-examples/was-userdata.sh") +} + diff --git a/terraform/env/prod/network.tf b/terraform/env/prod/network.tf new file mode 100644 index 0000000..75d39c7 --- /dev/null +++ b/terraform/env/prod/network.tf @@ -0,0 +1,263 @@ +# VPC +module "vpc" { + source = "../../modules/network/vpc" + cidr_block = "10.0.0.0/16" + name = "${local.name_prefix}-vpc" + purpose = "main" + environment = local.environment + tags = local.common_tags +} + +# Internet Gateway +module "igw" { + source = "../../modules/network/igw" + vpc_id = module.vpc.vpc_id + name = "${local.name_prefix}-igw" + purpose = "main" +} + +# Public Route Table +module "route_table_public" { + source = "../../modules/network/route_table" + vpc_id = module.vpc.vpc_id + gateway_id = module.igw.gateway_id + enable_igw_route = true + name = "${local.name_prefix}-public-rt" + access_level = "public" + purpose = "public" +} + +# Private Route Table +module "route_table_private" { + source = "../../modules/network/route_table" + vpc_id = module.vpc.vpc_id + enable_igw_route = false + name = "${local.name_prefix}-private-rt" + access_level = "private" + purpose = "private" +} + +# Public Subnets +module "subnet_public_a" { + source = "../../modules/network/subnet" + vpc_id = module.vpc.vpc_id + cidr_block = "10.0.1.0/24" + az = "ap-northeast-2a" + map_public_ip = true + name = "${local.name_prefix}-subnet-public-a" + route_table_id = module.route_table_public.route_table_id + environment = local.environment + purpose = "public" +} + +module "subnet_public_c" { + source = "../../modules/network/subnet" + vpc_id = module.vpc.vpc_id + cidr_block = "10.0.2.0/24" + az = "ap-northeast-2c" + map_public_ip = true + name = "${local.name_prefix}-subnet-public-c" + route_table_id = module.route_table_public.route_table_id + environment = local.environment + purpose = "public" +} + +# Private Subnets +module "subnet_private_a" { + source = "../../modules/network/subnet" + vpc_id = module.vpc.vpc_id + cidr_block = "10.0.11.0/24" + az = "ap-northeast-2a" + map_public_ip = false + name = "${local.name_prefix}-subnet-private-a" + route_table_id = module.route_table_private.route_table_id + environment = local.environment + purpose = "private" +} + +module "subnet_private_c" { + source = "../../modules/network/subnet" + vpc_id = module.vpc.vpc_id + cidr_block = "10.0.12.0/24" + az = "ap-northeast-2c" + map_public_ip = false + name = "${local.name_prefix}-subnet-private-c" + route_table_id = module.route_table_private.route_table_id + environment = local.environment + purpose = "private" +} + +# EC2 Security Group +module "sg_ec2" { + source = "../../modules/security/security_group" + vpc_id = module.vpc.vpc_id + + environment = local.environment + purpose = "ec2" + security_group_name = "${local.name_prefix}-sg-ec2" + + ingress_rules = [ + { + from_port = 80 + to_port = 80 + protocol = "tcp" + use_cidr = false + use_sg = true + source_security_group_id = module.sg_alb.security_group_id + }, + { + from_port = 22 + to_port = 22 + protocol = "tcp" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] + + egress_rules = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] +} + +# RDS Security Group +module "sg_rds" { + source = "../../modules/security/security_group" + vpc_id = module.vpc.vpc_id + + environment = local.environment + purpose = "rds" + security_group_name = "${local.name_prefix}-sg-rds" + + ingress_rules = [ + { + from_port = 3306 + to_port = 3306 + protocol = "tcp" + use_cidr = false + use_sg = true + source_security_group_id = module.sg_ec2.security_group_id + } + ] + + egress_rules = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] +} + +# ALB Security Group +module "sg_alb" { + source = "../../modules/security/security_group" + vpc_id = module.vpc.vpc_id + + environment = local.environment + purpose = "alb" + security_group_name = "${local.name_prefix}-sg-alb" + + ingress_rules = [ + { + from_port = 80 + to_port = 80 + protocol = "tcp" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + }, + { + from_port = 443 + to_port = 443 + protocol = "tcp" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] + + egress_rules = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + use_cidr = true + use_sg = false + cidr_blocks = ["0.0.0.0/0"] + } + ] +} + +# ACM Certificate +module "acm" { + source = "../../modules/network/acm" + + name_prefix = local.name_prefix + domain_name = var.domain_name + hosted_zone_id = module.route53_zone.hosted_zone_id + + tags = local.common_tags +} + +# Application Load Balancer +module "alb" { + source = "../../modules/network/alb" + + name_prefix = local.name_prefix + internal = false + security_groups = [module.sg_alb.security_group_id] + subnet_ids = [module.subnet_public_a.subnet_id, module.subnet_public_c.subnet_id] + vpc_id = module.vpc.vpc_id + + target_group_port = 80 + target_group_protocol = "HTTP" + + health_check_path = "/health" + health_check_matcher = "200" + + certificate_arn = module.acm.certificate_arn + + tags = local.common_tags +} + +# Route53 - Hosted Zone 생성 +module "route53_zone" { + source = "../../modules/network/route53" + + # 새로운 hosted zone 생성 + create_hosted_zone = true + domain_name = var.domain_name + create_a_record = false + + tags = local.common_tags +} + +# Route53 - ALB를 A 레코드로 설정 (ALB 생성 후) +module "route53_record" { + source = "../../modules/network/route53" + + # 기존 hosted zone 사용 + create_hosted_zone = false + hosted_zone_id = module.route53_zone.hosted_zone_id + + # A 레코드 생성 (ALB로 변경) + create_a_record = true + record_name = "${local.environment}.${var.domain_name}" + target_alias = module.alb.load_balancer_dns_name + target_zone_id = module.alb.load_balancer_zone_id + ttl = 300 + + depends_on = [module.alb] +} + diff --git a/terraform/env/prod/provider.tf b/terraform/env/prod/provider.tf new file mode 100644 index 0000000..3edd6d4 --- /dev/null +++ b/terraform/env/prod/provider.tf @@ -0,0 +1,11 @@ +provider "aws" { + region = local.aws_region + + access_key = var.aws_access_key_id + secret_key = var.aws_secret_access_key + + default_tags { + tags = local.common_tags + } +} + diff --git a/terraform/env/prod/storage.tf b/terraform/env/prod/storage.tf new file mode 100644 index 0000000..d42b352 --- /dev/null +++ b/terraform/env/prod/storage.tf @@ -0,0 +1,17 @@ +# S3 Bucket +module "s3" { + source = "../../modules/storage/s3" + bucket_name = "prod-clokey-storage-bucket" + environment = local.environment + purpose = "storage" + + # Public read access (GET only) - AWS SDK로 업로드/삭제, 외부에서 읽기만 허용 + enable_public_read = true + enable_block_public_access = false # public read를 위해 비활성화 + enable_versioning = true + enable_sse = true + sse_algorithm = "AES256" + + tags = local.common_tags +} + diff --git a/terraform/env/prod/terraform.tf b/terraform/env/prod/terraform.tf new file mode 100644 index 0000000..60997a7 --- /dev/null +++ b/terraform/env/prod/terraform.tf @@ -0,0 +1,11 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + diff --git a/terraform/env/prod/terraform.tfvars b/terraform/env/prod/terraform.tfvars new file mode 100644 index 0000000..78ce5d5 --- /dev/null +++ b/terraform/env/prod/terraform.tfvars @@ -0,0 +1,15 @@ +aws_region = "ap-northeast-2" +environment = "prod" +vpc_cidr_block = "10.0.0.0/16" +public_subnet_cidr = "10.0.1.0/24" +availability_zone = "ap-northeast-2a" + +# RDS 설정 (기본값 - 민감한 정보는 secret.tfvars에서 관리) +# rds_username은 secret.tfvars에서 관리 + +# Route53 설정 (기본값 - 민감한 정보는 secret.tfvars에서 관리) +# hosted_zone_id는 secret.tfvars에서 관리 +# domain_name은 secret.tfvars에서 관리 + +# User Data (기본값 - 민감한 정보는 secret.tfvars에서 관리) +# userdata는 locals.tf에서 filebase64() 함수로 로드됨 diff --git a/terraform/env/prod/variables.tf b/terraform/env/prod/variables.tf new file mode 100644 index 0000000..f1f8a76 --- /dev/null +++ b/terraform/env/prod/variables.tf @@ -0,0 +1,33 @@ +# AWS 인증 정보 (CI/CD에서 관리) +variable "aws_access_key_id" { + description = "AWS Access Key ID" + type = string + sensitive = true +} + +variable "aws_secret_access_key" { + description = "AWS Secret Access Key" + type = string + sensitive = true +} + +# 민감한 정보만 변수화 (CI/CD에서 관리) +variable "rds_username" { + description = "Username for RDS database" + type = string + sensitive = true +} + +variable "rds_password" { + description = "Password for RDS database" + type = string + sensitive = true +} + +# Route53 설정 (도메인 관련) +variable "domain_name" { + description = "Base domain name for Route53 records" + type = string + default = "example.com" + sensitive = true +} diff --git a/terraform/modules/compute/ec2/main.tf b/terraform/modules/compute/ec2/main.tf new file mode 100644 index 0000000..f96cdc6 --- /dev/null +++ b/terraform/modules/compute/ec2/main.tf @@ -0,0 +1,57 @@ +# EC2 인스턴스 생성 +resource "aws_instance" "this" { + ami = var.ami + instance_type = var.instance_type + subnet_id = var.subnet_id + vpc_security_group_ids = var.security_group_id_list + key_name = var.key_name + private_ip = var.private_ip + associate_public_ip_address = var.associate_public_ip_address + disable_api_termination = var.disable_api_termination + instance_initiated_shutdown_behavior = var.instance_initiated_shutdown_behavior + monitoring = var.monitoring + + # 사용자 데이터 설정 (변수로 주입받거나 기본 파일 사용) + # AWS는 user_data를 base64로 인코딩하여 전달해야 함 + user_data_base64 = var.user_data != null ? var.user_data : null + user_data_replace_on_change = false # UserData 변경해도도 인스턴스 유지 (수동 재시작 필요) + + # 루트 볼륨 설정 + root_block_device { + volume_size = var.root_volume_size + volume_type = var.root_volume_type + encrypted = var.root_volume_encrypted + delete_on_termination = var.root_volume_delete_on_termination + } + + tags = merge(var.tags, { + Name = var.name + }) +} + +# 추가 EBS 볼륨 생성 및 연결 +resource "aws_ebs_volume" "additional" { + count = length(var.additional_ebs_volumes) + + availability_zone = aws_instance.this.availability_zone + size = var.additional_ebs_volumes[count.index].size + type = var.additional_ebs_volumes[count.index].volume_type + encrypted = var.additional_ebs_volumes[count.index].encrypted + + tags = merge(var.additional_ebs_volumes[count.index].tags, { + Name = "clokey-${var.purpose}-${var.environment}-vol-${count.index + 1}" + InstanceName = var.name + }) +} + +# 추가 EBS 볼륨을 EC2 인스턴스에 연결 +resource "aws_volume_attachment" "additional" { + count = length(var.additional_ebs_volumes) + + device_name = var.additional_ebs_volumes[count.index].device_name + volume_id = aws_ebs_volume.additional[count.index].id + instance_id = aws_instance.this.id + + # 인스턴스가 중지된 상태에서만 볼륨 분리 가능 + stop_instance_before_detaching = true +} diff --git a/terraform/modules/compute/ec2/output.tf b/terraform/modules/compute/ec2/output.tf new file mode 100644 index 0000000..85a4046 --- /dev/null +++ b/terraform/modules/compute/ec2/output.tf @@ -0,0 +1,47 @@ +output "instance_id" { + description = "EC2 Instance ID" + value = aws_instance.this.id +} + +output "public_ip" { + description = "EC2 Instance Public IP" + value = aws_instance.this.public_ip +} + +output "private_ip" { + description = "EC2 Instance Private IP" + value = aws_instance.this.private_ip +} + +output "availability_zone" { + description = "EC2 Instance Availability Zone" + value = aws_instance.this.availability_zone +} + +output "subnet_id" { + description = "EC2 Instance Subnet ID" + value = aws_instance.this.subnet_id +} + +output "arn" { + description = "EC2 Instance ARN" + value = aws_instance.this.arn +} + +output "instance_state" { + description = "EC2 Instance State" + value = aws_instance.this.instance_state +} + +output "root_block_device" { + description = "EC2 Instance Root Block Device" + value = aws_instance.this.root_block_device +} + +output "additional_ebs_volumes" { + description = "Additional EBS Volumes" + value = { + volumes = aws_ebs_volume.additional[*].id + attachments = aws_volume_attachment.additional[*].id + } +} diff --git a/terraform/modules/compute/ec2/variables.tf b/terraform/modules/compute/ec2/variables.tf new file mode 100644 index 0000000..b7dc5f9 --- /dev/null +++ b/terraform/modules/compute/ec2/variables.tf @@ -0,0 +1,128 @@ +variable "ami" { type = string } + +variable "instance_type" { type = string } + +variable "subnet_id" { type = string } + +variable "security_group_id_list" { + description = "Security Group ID List" + type = list(string) +} + +variable "name" { type = string } + +variable "environment" { + description = "Environment name (ex: dev or prod)" + type = string +} + +variable "purpose" { + description = "Usage purpose (ex: web, api, db)" + type = string +} + +# 볼륨 설정 +variable "root_volume_size" { + description = "Size of the root volume in GB" + type = number + default = 8 +} + +variable "root_volume_type" { + description = "Type of the root volume (gp2, gp3, io1, io2, standard)" + type = string + default = "gp3" + validation { + condition = contains(["gp2", "gp3", "io1", "io2", "standard"], var.root_volume_type) + error_message = "Volume type must be one of: gp2, gp3, io1, io2, standard." + } +} + +variable "root_volume_encrypted" { + description = "Whether to encrypt the root volume" + type = bool + default = true +} + +variable "root_volume_delete_on_termination" { + description = "Whether to delete the root volume when the instance is terminated" + type = bool + default = true +} + +# 추가 EBS 볼륨 설정 +variable "additional_ebs_volumes" { + description = "List of additional EBS volumes to attach" + type = list(object({ + device_name = string + size = number + volume_type = string + encrypted = bool + delete_on_termination = bool + tags = map(string) + })) + default = [] +} + +# 네트워크 설정 +variable "associate_public_ip_address" { + description = "Whether to associate a public IP address with the instance" + type = bool + default = false +} + +variable "private_ip" { + description = "Private IP address to associate with the instance" + type = string + default = null +} + +# SSH 키 설정 +variable "key_name" { + description = "Name of the SSH key pair to use for the instance" + type = string + default = null +} + +# 인스턴스 설정 +variable "disable_api_termination" { + description = "Whether to disable API termination of the instance" + type = bool + default = false +} + +variable "instance_initiated_shutdown_behavior" { + description = "Shutdown behavior for the instance (stop or terminate)" + type = string + default = "stop" + validation { + condition = contains(["stop", "terminate"], var.instance_initiated_shutdown_behavior) + error_message = "Shutdown behavior must be either 'stop' or 'terminate'." + } +} + +variable "monitoring" { + description = "Whether to enable detailed monitoring" + type = bool + default = false +} + +# 사용자 데이터 설정 +variable "user_data" { + description = "User data script content (base64 encoded). Use filebase64() function to encode the file." + type = string + default = null + sensitive = true +} + +variable "user_data_replace_on_change" { + description = "Whether to replace the instance when user data changes" + type = bool + default = false +} + +variable "tags" { + description = "Additional tags for the EC2 instance" + type = map(string) + default = {} +} diff --git a/terraform/modules/database/rds/main.tf b/terraform/modules/database/rds/main.tf new file mode 100644 index 0000000..aad7265 --- /dev/null +++ b/terraform/modules/database/rds/main.tf @@ -0,0 +1,65 @@ +# DB 파라미터 그룹 생성 (선택적) +resource "aws_db_parameter_group" "main" { + count = var.parameter_group_family != null ? 1 : 0 + + family = var.parameter_group_family + name = "${var.name}-parameter-group" + + dynamic "parameter" { + for_each = var.parameter_group_parameters + content { + name = parameter.value.name + value = parameter.value.value + } + } + + tags = merge(var.tags, { Name = "${var.name}-parameter-group" }) +} + +# DB 서브넷 그룹 +resource "aws_db_subnet_group" "this" { + name = var.name + subnet_ids = var.subnet_ids + tags = merge(var.tags, { Name = var.name }) +} + +# RDS 인스턴스 +resource "aws_db_instance" "this" { + identifier = var.name + allocated_storage = var.storage + engine = var.engine + engine_version = var.engine_version + instance_class = var.instance_class + db_name = var.db_name + username = var.username + + password = var.password + db_subnet_group_name = aws_db_subnet_group.this.name + vpc_security_group_ids = [var.security_group_id] + + # 네트워크 설정 + publicly_accessible = var.publicly_accessible + port = var.port + + # 백업 및 스냅샷 설정 + backup_retention_period = var.backup_retention_period + backup_window = var.backup_window + maintenance_window = var.maintenance_window + skip_final_snapshot = var.skip_final_snapshot + final_snapshot_identifier = var.final_snapshot_identifier + + # 성능 설정 + multi_az = var.multi_az + storage_type = var.storage_type + storage_encrypted = var.storage_encrypted + iops = var.iops + + # 파라미터 그룹 설정 + parameter_group_name = var.parameter_group_family != null ? aws_db_parameter_group.main[0].name : null + + # 보안 설정 + deletion_protection = var.deletion_protection + auto_minor_version_upgrade = var.auto_minor_version_upgrade + + tags = merge(var.tags, { Name = var.name }) +} diff --git a/terraform/modules/database/rds/output.tf b/terraform/modules/database/rds/output.tf new file mode 100644 index 0000000..ede420d --- /dev/null +++ b/terraform/modules/database/rds/output.tf @@ -0,0 +1,55 @@ +output "endpoint" { + description = "RDS instance endpoint" + value = aws_db_instance.this.endpoint +} + +output "port" { + description = "RDS instance port" + value = aws_db_instance.this.port +} + +output "database_name" { + description = "RDS instance database name" + value = aws_db_instance.this.db_name +} + +output "username" { + description = "RDS instance master username" + value = aws_db_instance.this.username + sensitive = true +} + +output "arn" { + description = "RDS instance ARN" + value = aws_db_instance.this.arn +} + +output "id" { + description = "RDS instance ID" + value = aws_db_instance.this.id +} + +output "resource_id" { + description = "RDS instance resource ID" + value = aws_db_instance.this.resource_id +} + +output "status" { + description = "RDS instance status" + value = aws_db_instance.this.status +} + +output "availability_zone" { + description = "RDS instance availability zone" + value = aws_db_instance.this.availability_zone +} + +output "subnet_group_name" { + description = "RDS subnet group name" + value = aws_db_subnet_group.this.name +} + +output "parameter_group_name" { + description = "RDS parameter group name" + value = var.parameter_group_family != null ? aws_db_parameter_group.main[0].name : null +} diff --git a/terraform/modules/database/rds/variables.tf b/terraform/modules/database/rds/variables.tf new file mode 100644 index 0000000..78e7fce --- /dev/null +++ b/terraform/modules/database/rds/variables.tf @@ -0,0 +1,146 @@ +variable "name" { type = string } + +variable "subnet_ids" { type = list(string) } + +variable "storage" { type = number } + +variable "engine" { type = string } + +variable "engine_version" { + description = "Database engine version" + type = string + default = null +} + +variable "instance_class" { type = string } + +variable "db_name" { type = string } + +variable "username" { type = string } + +variable "password" { + description = "Password for RDS database" + type = string + sensitive = true +} + +variable "security_group_id" { type = string } + +variable "environment" { + description = "Environment name (ex: dev or prod)" + type = string +} + +variable "purpose" { + description = "Usage purpose (ex: app, analytics)" + type = string + default = "app" +} + +# 네트워크 설정 +variable "publicly_accessible" { + description = "Whether the database is publicly accessible" + type = bool + default = false +} + +variable "port" { + description = "Database port" + type = number + default = null +} + +# 백업 및 스냅샷 설정 +variable "backup_retention_period" { + description = "Number of days to retain backups" + type = number + default = 7 +} + +variable "backup_window" { + description = "Preferred backup window" + type = string + default = "03:00-04:00" +} + +variable "maintenance_window" { + description = "Preferred maintenance window" + type = string + default = "sun:04:00-sun:05:00" +} + +variable "skip_final_snapshot" { + description = "Whether to skip final snapshot when deleting the database" + type = bool + default = true +} + +variable "final_snapshot_identifier" { + description = "Name of final snapshot when skip_final_snapshot is false" + type = string + default = null +} + +# 성능 설정 +variable "multi_az" { + description = "Whether to enable Multi-AZ deployment" + type = bool + default = false +} + +variable "storage_type" { + description = "Storage type (gp2, gp3, io1)" + type = string + default = "gp3" + validation { + condition = contains(["gp2", "gp3", "io1"], var.storage_type) + error_message = "Storage type must be one of: gp2, gp3, io1." + } +} + +variable "storage_encrypted" { + description = "Whether to encrypt the storage" + type = bool + default = true +} + +variable "iops" { + description = "IOPS for io1 storage type" + type = number + default = null +} + +# 파라미터 그룹 설정 +variable "parameter_group_family" { + description = "Parameter group family (e.g., mysql8.0, postgres13)" + type = string + default = null +} + +variable "parameter_group_parameters" { + description = "List of parameters to set in the parameter group" + type = list(object({ + name = string + value = string + })) + default = [] +} + +# 보안 설정 +variable "deletion_protection" { + description = "Whether to enable deletion protection" + type = bool + default = false +} + +variable "auto_minor_version_upgrade" { + description = "Whether to enable auto minor version upgrade" + type = bool + default = true +} + +variable "tags" { + description = "Additional tags for the RDS resources" + type = map(string) + default = {} +} diff --git a/terraform/modules/network/acm/main.tf b/terraform/modules/network/acm/main.tf new file mode 100644 index 0000000..334204b --- /dev/null +++ b/terraform/modules/network/acm/main.tf @@ -0,0 +1,44 @@ +resource "aws_acm_certificate" "main" { + domain_name = var.domain_name + validation_method = "DNS" + + subject_alternative_names = var.subject_alternative_names + + lifecycle { + create_before_destroy = true + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-cert" + }) +} + +resource "aws_acm_certificate_validation" "main" { + count = var.create_validation ? 1 : 0 + + certificate_arn = aws_acm_certificate.main.arn + validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] + + timeouts { + create = "20m" # 타임아웃을 20분으로 증가 + } + + depends_on = [aws_route53_record.cert_validation] +} + +resource "aws_route53_record" "cert_validation" { + for_each = { + for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = var.hosted_zone_id +} diff --git a/terraform/modules/network/acm/output.tf b/terraform/modules/network/acm/output.tf new file mode 100644 index 0000000..2f37bec --- /dev/null +++ b/terraform/modules/network/acm/output.tf @@ -0,0 +1,19 @@ +output "certificate_arn" { + description = "ARN of the ACM certificate" + value = aws_acm_certificate.main.arn +} + +output "certificate_domain_name" { + description = "Domain name of the certificate" + value = aws_acm_certificate.main.domain_name +} + +output "certificate_validation_status" { + description = "Validation status of the certificate" + value = aws_acm_certificate.main.status +} + +output "certificate_validation_records" { + description = "DNS validation records for the certificate" + value = aws_route53_record.cert_validation +} diff --git a/terraform/modules/network/acm/variables.tf b/terraform/modules/network/acm/variables.tf new file mode 100644 index 0000000..de352d0 --- /dev/null +++ b/terraform/modules/network/acm/variables.tf @@ -0,0 +1,32 @@ +variable "name_prefix" { + description = "Name prefix for resources" + type = string +} + +variable "domain_name" { + description = "Primary domain name for the certificate" + type = string +} + +variable "subject_alternative_names" { + description = "List of subject alternative names for the certificate" + type = list(string) + default = [] +} + +variable "hosted_zone_id" { + description = "Route53 hosted zone ID for DNS validation" + type = string +} + +variable "create_validation" { + description = "Whether to create certificate validation" + type = bool + default = true +} + +variable "tags" { + description = "Tags to apply to resources" + type = map(string) + default = {} +} diff --git a/terraform/modules/network/alb/main.tf b/terraform/modules/network/alb/main.tf new file mode 100644 index 0000000..2816bce --- /dev/null +++ b/terraform/modules/network/alb/main.tf @@ -0,0 +1,96 @@ +resource "aws_lb" "main" { + name = "${var.name_prefix}-alb" + internal = var.internal + load_balancer_type = "application" + security_groups = var.security_groups + subnets = var.subnet_ids + + enable_deletion_protection = var.enable_deletion_protection + + access_logs { + bucket = var.access_logs_bucket + prefix = var.access_logs_prefix + enabled = var.enable_access_logs + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-alb" + }) +} + +resource "aws_lb_target_group" "main" { + name = "${var.name_prefix}-tg" + port = var.target_group_port + protocol = var.target_group_protocol + vpc_id = var.vpc_id + + health_check { + enabled = true + healthy_threshold = var.health_check_healthy_threshold + unhealthy_threshold = var.health_check_unhealthy_threshold + timeout = var.health_check_timeout + interval = var.health_check_interval + path = var.health_check_path + matcher = var.health_check_matcher + port = var.health_check_port + protocol = var.health_check_protocol + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-tg" + }) +} + +resource "aws_lb_listener" "http" { + count = var.create_http_listener ? 1 : 0 + + load_balancer_arn = aws_lb.main.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "redirect" + + redirect { + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } +} + +resource "aws_lb_listener" "https" { + count = var.create_https_listener ? 1 : 0 + + load_balancer_arn = aws_lb.main.arn + port = "443" + protocol = "HTTPS" + ssl_policy = var.ssl_policy + certificate_arn = var.certificate_arn + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.main.arn + } + + # 인증서가 완전히 검증된 후에 리스너 생성 + depends_on = [var.certificate_arn] +} + +resource "aws_lb_listener_rule" "main" { + count = length(var.listener_rules) + + listener_arn = var.create_https_listener ? aws_lb_listener.https[0].arn : aws_lb_listener.http[0].arn + priority = var.listener_rules[count.index].priority + + action { + type = "forward" + target_group_arn = aws_lb_target_group.main.arn + } + + condition { + path_pattern { + values = var.listener_rules[count.index].path_patterns + } + } +} diff --git a/terraform/modules/network/alb/output.tf b/terraform/modules/network/alb/output.tf new file mode 100644 index 0000000..974ca06 --- /dev/null +++ b/terraform/modules/network/alb/output.tf @@ -0,0 +1,34 @@ +output "load_balancer_arn" { + description = "ARN of the load balancer" + value = aws_lb.main.arn +} + +output "load_balancer_dns_name" { + description = "DNS name of the load balancer" + value = aws_lb.main.dns_name +} + +output "load_balancer_zone_id" { + description = "Zone ID of the load balancer" + value = aws_lb.main.zone_id +} + +output "target_group_arn" { + description = "ARN of the target group" + value = aws_lb_target_group.main.arn +} + +output "target_group_name" { + description = "Name of the target group" + value = aws_lb_target_group.main.name +} + +output "http_listener_arn" { + description = "ARN of the HTTP listener" + value = var.create_http_listener ? aws_lb_listener.http[0].arn : null +} + +output "https_listener_arn" { + description = "ARN of the HTTPS listener" + value = var.create_https_listener ? aws_lb_listener.https[0].arn : null +} diff --git a/terraform/modules/network/alb/variables.tf b/terraform/modules/network/alb/variables.tf new file mode 100644 index 0000000..65af55e --- /dev/null +++ b/terraform/modules/network/alb/variables.tf @@ -0,0 +1,148 @@ +variable "name_prefix" { + description = "Name prefix for resources" + type = string +} + +variable "internal" { + description = "Whether the load balancer is internal" + type = bool + default = false +} + +variable "security_groups" { + description = "List of security group IDs for the load balancer" + type = list(string) +} + +variable "subnet_ids" { + description = "List of subnet IDs for the load balancer" + type = list(string) +} + +variable "vpc_id" { + description = "VPC ID where the load balancer will be created" + type = string +} + +variable "enable_deletion_protection" { + description = "Enable deletion protection for the load balancer" + type = bool + default = false +} + +variable "access_logs_bucket" { + description = "S3 bucket for access logs" + type = string + default = "" +} + +variable "access_logs_prefix" { + description = "S3 prefix for access logs" + type = string + default = "" +} + +variable "enable_access_logs" { + description = "Enable access logs" + type = bool + default = false +} + +variable "target_group_port" { + description = "Port for the target group" + type = number + default = 80 +} + +variable "target_group_protocol" { + description = "Protocol for the target group" + type = string + default = "HTTP" +} + +variable "health_check_healthy_threshold" { + description = "Number of consecutive health checks successes required" + type = number + default = 2 +} + +variable "health_check_unhealthy_threshold" { + description = "Number of consecutive health check failures required" + type = number + default = 2 +} + +variable "health_check_timeout" { + description = "Amount of time to wait when receiving a response from a health check" + type = number + default = 5 +} + +variable "health_check_interval" { + description = "Approximate amount of time between health checks" + type = number + default = 30 +} + +variable "health_check_path" { + description = "Destination for the health check request" + type = string + default = "/" +} + +variable "health_check_matcher" { + description = "HTTP codes to use when checking for a successful response" + type = string + default = "200" +} + +variable "health_check_port" { + description = "Port to use to connect with the target" + type = string + default = "traffic-port" +} + +variable "health_check_protocol" { + description = "Protocol to use to connect with the target" + type = string + default = "HTTP" +} + +variable "create_http_listener" { + description = "Whether to create HTTP listener" + type = bool + default = true +} + +variable "create_https_listener" { + description = "Whether to create HTTPS listener" + type = bool + default = true +} + +variable "ssl_policy" { + description = "SSL policy for HTTPS listener" + type = string + default = "ELBSecurityPolicy-TLS-1-2-2017-01" +} + +variable "certificate_arn" { + description = "ARN of the SSL certificate for HTTPS listener" + type = string + default = "" +} + +variable "listener_rules" { + description = "List of listener rules" + type = list(object({ + priority = number + path_patterns = list(string) + })) + default = [] +} + +variable "tags" { + description = "Tags to apply to resources" + type = map(string) + default = {} +} diff --git a/terraform/modules/network/igw/main.tf b/terraform/modules/network/igw/main.tf new file mode 100644 index 0000000..e49dfdf --- /dev/null +++ b/terraform/modules/network/igw/main.tf @@ -0,0 +1,6 @@ +resource "aws_internet_gateway" "this" { + vpc_id = var.vpc_id + tags = merge(var.tags, { + Name = var.name + }) +} diff --git a/terraform/modules/network/igw/output.tf b/terraform/modules/network/igw/output.tf new file mode 100644 index 0000000..32daa0b --- /dev/null +++ b/terraform/modules/network/igw/output.tf @@ -0,0 +1,4 @@ +output "gateway_id" { + description = "Internet Gateway ID" + value = aws_internet_gateway.this.id +} diff --git a/terraform/modules/network/igw/variables.tf b/terraform/modules/network/igw/variables.tf new file mode 100644 index 0000000..262f4aa --- /dev/null +++ b/terraform/modules/network/igw/variables.tf @@ -0,0 +1,27 @@ +variable "vpc_id" { + description = "VPC ID where the internet gateway will be attached" + type = string +} + +variable "name" { + description = "Name of the internet gateway" + type = string +} + +variable "environment" { + description = "Environment name (ex: dev or prod) - optional for shared resources" + type = string + default = null +} + +variable "purpose" { + description = "Usage purpose (ex: main)" + type = string + default = "main" +} + +variable "tags" { + description = "Additional tags for the internet gateway" + type = map(string) + default = {} +} diff --git a/terraform/modules/network/route53/main.tf b/terraform/modules/network/route53/main.tf new file mode 100644 index 0000000..eebef58 --- /dev/null +++ b/terraform/modules/network/route53/main.tf @@ -0,0 +1,33 @@ +# Route53 Hosted Zone +resource "aws_route53_zone" "main" { + count = var.create_hosted_zone ? 1 : 0 + name = var.domain_name + + tags = var.tags +} + +# Route53 A Record (IP) +resource "aws_route53_record" "a" { + count = var.create_a_record && var.target_ip != null && var.record_name != null ? 1 : 0 + + zone_id = var.hosted_zone_id != null ? var.hosted_zone_id : (var.create_hosted_zone ? aws_route53_zone.main[0].zone_id : null) + name = var.record_name + type = "A" + ttl = var.ttl + records = [var.target_ip] +} + +# Route53 A Record (ALB Alias) +resource "aws_route53_record" "alias" { + count = var.create_a_record && var.record_name != null ? 1 : 0 + + zone_id = var.hosted_zone_id != null ? var.hosted_zone_id : (var.create_hosted_zone ? aws_route53_zone.main[0].zone_id : null) + name = var.record_name + type = "A" + + alias { + name = var.target_alias + zone_id = var.target_zone_id + evaluate_target_health = true + } +} diff --git a/terraform/modules/network/route53/output.tf b/terraform/modules/network/route53/output.tf new file mode 100644 index 0000000..c42c952 --- /dev/null +++ b/terraform/modules/network/route53/output.tf @@ -0,0 +1,25 @@ +output "hosted_zone_id" { + description = "Route53 hosted zone ID" + value = var.create_hosted_zone ? aws_route53_zone.main[0].zone_id : var.hosted_zone_id +} + +output "name_servers" { + description = "Name servers for the hosted zone" + value = var.create_hosted_zone ? aws_route53_zone.main[0].name_servers : null +} + +output "domain_name" { + description = "Domain name of the hosted zone" + value = var.create_hosted_zone ? aws_route53_zone.main[0].name : var.domain_name +} + +output "record_name" { + description = "Name of the A record" + value = var.create_a_record ? ( + var.target_ip != null ? ( + length(aws_route53_record.a) > 0 ? aws_route53_record.a[0].name : null + ) : ( + length(aws_route53_record.alias) > 0 ? aws_route53_record.alias[0].name : null + ) + ) : null +} diff --git a/terraform/modules/network/route53/variables.tf b/terraform/modules/network/route53/variables.tf new file mode 100644 index 0000000..103dbbb --- /dev/null +++ b/terraform/modules/network/route53/variables.tf @@ -0,0 +1,59 @@ +variable "create_hosted_zone" { + description = "Whether to create a new hosted zone" + type = bool + default = false +} + +variable "domain_name" { + description = "Domain name for the hosted zone" + type = string + default = null +} + +variable "hosted_zone_id" { + description = "Existing hosted zone ID (if not creating new one)" + type = string + default = null +} + +variable "create_a_record" { + description = "Whether to create an A record" + type = bool + default = true +} + +variable "record_name" { + description = "Name for the A record (e.g., 'api.example.com')" + type = string + default = null +} + +variable "target_ip" { + description = "Target IP address for the A record" + type = string + default = null +} + +variable "target_alias" { + description = "Target alias (e.g., ALB DNS name) for the A record" + type = string + default = null +} + +variable "target_zone_id" { + description = "Target zone ID for alias records (e.g., ALB zone ID)" + type = string + default = null +} + +variable "ttl" { + description = "TTL for the A record" + type = number + default = 300 +} + +variable "tags" { + description = "Additional tags for the Route53 resources" + type = map(string) + default = {} +} diff --git a/terraform/modules/network/route_table/main.tf b/terraform/modules/network/route_table/main.tf new file mode 100644 index 0000000..0e65c9e --- /dev/null +++ b/terraform/modules/network/route_table/main.tf @@ -0,0 +1,14 @@ +resource "aws_route_table" "this" { + vpc_id = var.vpc_id + + tags = merge(var.tags, { + Name = var.name + }) +} + +resource "aws_route" "igw" { + count = var.enable_igw_route ? 1 : 0 + route_table_id = aws_route_table.this.id + destination_cidr_block = "0.0.0.0/0" + gateway_id = var.gateway_id +} diff --git a/terraform/modules/network/route_table/output.tf b/terraform/modules/network/route_table/output.tf new file mode 100644 index 0000000..61e9111 --- /dev/null +++ b/terraform/modules/network/route_table/output.tf @@ -0,0 +1,4 @@ +output "route_table_id" { + description = "Route table ID" + value = aws_route_table.this.id +} diff --git a/terraform/modules/network/route_table/variables.tf b/terraform/modules/network/route_table/variables.tf new file mode 100644 index 0000000..d86b3e9 --- /dev/null +++ b/terraform/modules/network/route_table/variables.tf @@ -0,0 +1,49 @@ +variable "vpc_id" { + type = string + description = "VPC ID for the route table" +} + +variable "access_level" { + description = "Internet access enabled (예: public, private)" + type = string +} + +variable "gateway_id" { + type = string + default = "" + description = "Internet Gateway ID for default route" +} + +variable "destination_cidr_block" { + description = "(option) IGW route CIDR (default: 0.0.0.0/0)" + type = string + default = "0.0.0.0/0" +} + +variable "name" { + type = string + description = "Name tag" +} + +variable "enable_igw_route" { + description = "Whether IGW routing is added" + type = bool + default = false +} + +variable "environment" { + description = "Environment name (ex: dev or prod) - optional for shared resources" + type = string + default = null +} + +variable "purpose" { + description = "Usage purpose (ex: public, private)" + type = string +} + +variable "tags" { + description = "Additional tags for the route table" + type = map(string) + default = {} +} diff --git a/terraform/modules/network/subnet/main.tf b/terraform/modules/network/subnet/main.tf new file mode 100644 index 0000000..e9ead1a --- /dev/null +++ b/terraform/modules/network/subnet/main.tf @@ -0,0 +1,15 @@ +resource "aws_subnet" "this" { + vpc_id = var.vpc_id + cidr_block = var.cidr_block + availability_zone = var.az + map_public_ip_on_launch = var.map_public_ip + + tags = merge(var.tags, { + Name = var.name + }) +} + +resource "aws_route_table_association" "this" { + subnet_id = aws_subnet.this.id + route_table_id = var.route_table_id +} \ No newline at end of file diff --git a/terraform/modules/network/subnet/output.tf b/terraform/modules/network/subnet/output.tf new file mode 100644 index 0000000..1d94a7b --- /dev/null +++ b/terraform/modules/network/subnet/output.tf @@ -0,0 +1,4 @@ +output "subnet_id" { + description = "Subnet ID" + value = aws_subnet.this.id +} diff --git a/terraform/modules/network/subnet/variables.tf b/terraform/modules/network/subnet/variables.tf new file mode 100644 index 0000000..c682d1d --- /dev/null +++ b/terraform/modules/network/subnet/variables.tf @@ -0,0 +1,45 @@ +variable "vpc_id" { + description = "VPC ID where the subnet will be created" + type = string +} + +variable "cidr_block" { + description = "CIDR block for the subnet" + type = string +} + +variable "az" { + description = "Availability zone for the subnet" + type = string +} + +variable "map_public_ip" { + description = "Whether to map public IP on launch" + type = bool +} + +variable "name" { + description = "Name of the subnet" + type = string +} + +variable "route_table_id" { + description = "Route table ID to associate with the subnet" + type = string +} + +variable "environment" { + description = "Environment name (ex: dev or prod)" + type = string +} + +variable "purpose" { + description = "Usage purpose (ex: public, private)" + type = string +} + +variable "tags" { + description = "Additional tags for the subnet" + type = map(string) + default = {} +} diff --git a/terraform/modules/network/vpc/main.tf b/terraform/modules/network/vpc/main.tf new file mode 100644 index 0000000..3841262 --- /dev/null +++ b/terraform/modules/network/vpc/main.tf @@ -0,0 +1,9 @@ +resource "aws_vpc" "this" { + cidr_block = var.cidr_block + enable_dns_support = true + enable_dns_hostnames = true + + tags = merge(var.tags, { + Name = var.name + }) +} diff --git a/terraform/modules/network/vpc/output.tf b/terraform/modules/network/vpc/output.tf new file mode 100644 index 0000000..586fa59 --- /dev/null +++ b/terraform/modules/network/vpc/output.tf @@ -0,0 +1,4 @@ +output "vpc_id" { + description = "VPC ID" + value = aws_vpc.this.id +} \ No newline at end of file diff --git a/terraform/modules/network/vpc/variables.tf b/terraform/modules/network/vpc/variables.tf new file mode 100644 index 0000000..53de3dc --- /dev/null +++ b/terraform/modules/network/vpc/variables.tf @@ -0,0 +1,27 @@ +variable "cidr_block" { + description = "CIDR block for the VPC" + type = string +} + +variable "name" { + description = "Name of the VPC" + type = string +} + +variable "environment" { + description = "Environment name (ex: dev or prod) - optional for shared resources" + type = string + default = null +} + +variable "purpose" { + description = "Usage purpose (ex: main, isolated)" + type = string + default = "main" +} + +variable "tags" { + description = "Additional tags for the VPC" + type = map(string) + default = {} +} diff --git a/terraform/modules/security/security_group/main.tf b/terraform/modules/security/security_group/main.tf new file mode 100644 index 0000000..2e030a3 --- /dev/null +++ b/terraform/modules/security/security_group/main.tf @@ -0,0 +1,31 @@ +resource "aws_security_group" "this" { + name = var.security_group_name + vpc_id = var.vpc_id + + # 동적 인그레스 규칙 생성 + dynamic "ingress" { + for_each = var.ingress_rules + content { + from_port = ingress.value.from_port + to_port = ingress.value.to_port + protocol = ingress.value.protocol + cidr_blocks = ingress.value.use_cidr ? ingress.value.cidr_blocks : null + security_groups = ingress.value.use_sg ? [ingress.value.source_security_group_id] : null + } + } + + # 동적 이그레스 규칙 생성 + dynamic "egress" { + for_each = var.egress_rules + content { + from_port = egress.value.from_port + to_port = egress.value.to_port + protocol = egress.value.protocol + cidr_blocks = egress.value.cidr_blocks + } + } + + tags = merge(var.tags, { + Name = var.security_group_name + }) +} diff --git a/terraform/modules/security/security_group/output.tf b/terraform/modules/security/security_group/output.tf new file mode 100644 index 0000000..fa168e0 --- /dev/null +++ b/terraform/modules/security/security_group/output.tf @@ -0,0 +1,4 @@ +output "security_group_id" { + description = "Security Group ID" + value = aws_security_group.this.id +} diff --git a/terraform/modules/security/security_group/variables.tf b/terraform/modules/security/security_group/variables.tf new file mode 100644 index 0000000..af6f956 --- /dev/null +++ b/terraform/modules/security/security_group/variables.tf @@ -0,0 +1,56 @@ +variable "security_group_name" { + description = "Name of security group" + type = string +} + +variable "environment" { + description = "Environment name (ex: dev or prod)" + type = string +} + +variable "purpose" { + description = "Usage purpose (ex: jenkins, db, bastion)" + type = string +} + +variable "vpc_id" { + description = "VPC ID" + type = string +} + +variable "ingress_rules" { + description = "Inbounds rule list" + type = list(object({ + from_port = number + to_port = number + protocol = string + use_cidr = bool + use_sg = bool + cidr_blocks = optional(list(string), []) + source_security_group_id = optional(string) + })) +} + +variable "egress_rules" { + description = "Outbounds rule list" + type = list(object({ + from_port = number + to_port = number + protocol = string + cidr_blocks = list(string) + })) + default = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + ] +} + +variable "tags" { + description = "Additional tags for the security group" + type = map(string) + default = {} +} diff --git a/terraform/modules/storage/s3/README.md b/terraform/modules/storage/s3/README.md new file mode 100644 index 0000000..0c9a64c --- /dev/null +++ b/terraform/modules/storage/s3/README.md @@ -0,0 +1,113 @@ +# S3 Bucket Module + +이 모듈은 AWS S3 버킷을 생성하고 보안 설정을 자동으로 구성합니다. + +## 기능 + +- S3 버킷 생성 +- 버전 관리 설정 (선택적) +- 서버 사이드 암호화 설정 (선택적) +- 공개 액세스 차단 설정 (선택적) +- 수명 주기 정책 설정 (선택적) +- 자동 태그 설정 + +## 사용법 + +### 기본 사용법 + +```hcl +module "s3_bucket" { + source = "../../modules/storage/s3" + + bucket_name = "my-example-bucket" + environment = "dev" + purpose = "backup" +} +``` + +### Terraform State 버킷 생성 + +```hcl +module "tf_state_bucket" { + source = "../../modules/storage/s3" + + bucket_name = "my-terraform-state-bucket" + environment = "prod" + purpose = "tfstate" + + # 보안 설정 + enable_versioning = true + enable_sse = true + sse_algorithm = "AES256" + enable_block_public_access = true +} +``` + +### 이미지 저장용 버킷 (수명 주기 정책 포함) + +```hcl +module "image_bucket" { + source = "../../modules/storage/s3" + + bucket_name = "my-image-bucket" + environment = "prod" + purpose = "image" + + enable_versioning = true + enable_sse = true + enable_block_public_access = true + enable_lifecycle_policy = true + + lifecycle_rules = [ + { + id = "image-lifecycle" + status = "Enabled" + transitions = [ + { + days = 30 + storage_class = "STANDARD_IA" + }, + { + days = 90 + storage_class = "GLACIER" + } + ] + expiration = { + days = 365 + } + } + ] +} +``` + +## 변수 + +| 변수명 | 설명 | 타입 | 기본값 | 필수 | +|--------|------|------|--------|------| +| bucket_name | S3 버킷 이름 | string | - | ✅ | +| environment | 환경명 (dev, prod) | string | - | ✅ | +| purpose | 용도 (tfstate, image, backup 등) | string | - | ✅ | +| tags | 추가 태그 | map(string) | {} | ❌ | +| enable_versioning | 버전 관리 활성화 | bool | false | ❌ | +| enable_sse | 서버 사이드 암호화 활성화 | bool | false | ❌ | +| sse_algorithm | 암호화 알고리즘 (AES256, aws:kms) | string | "AES256" | ❌ | +| enable_block_public_access | 공개 액세스 차단 | bool | true | ❌ | +| enable_lifecycle_policy | 수명 주기 정책 활성화 | bool | false | ❌ | +| lifecycle_rules | 수명 주기 정책 규칙 | list(object) | [] | ❌ | + +## 출력값 + +| 출력값 | 설명 | +|--------|------| +| bucket_arn | 버킷 ARN | +| bucket_name | 버킷 이름 | +| bucket_id | 버킷 ID | +| bucket_domain_name | 버킷 도메인 이름 | +| bucket_regional_domain_name | 지역별 버킷 도메인 이름 | +| bucket_region | 버킷 지역 | + +## 보안 고려사항 + +- 기본적으로 공개 액세스가 차단됩니다 +- 서버 사이드 암호화를 활성화하는 것을 권장합니다 +- Terraform state 버킷의 경우 반드시 버전 관리와 암호화를 활성화하세요 diff --git a/terraform/modules/storage/s3/main.tf b/terraform/modules/storage/s3/main.tf new file mode 100644 index 0000000..0f9a1b5 --- /dev/null +++ b/terraform/modules/storage/s3/main.tf @@ -0,0 +1,92 @@ +# S3 버킷 생성 +resource "aws_s3_bucket" "this" { + bucket = var.bucket_name + + tags = merge(var.tags, { + Name = var.bucket_name + }) +} + +# S3 버킷 버전 관리 설정 +resource "aws_s3_bucket_versioning" "this" { + count = var.enable_versioning ? 1 : 0 + bucket = aws_s3_bucket.this.id + + versioning_configuration { + status = "Enabled" + } +} + +# S3 버킷 서버 사이드 암호화 설정 +resource "aws_s3_bucket_server_side_encryption_configuration" "this" { + count = var.enable_sse ? 1 : 0 + bucket = aws_s3_bucket.this.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = var.sse_algorithm + } + } +} + +# S3 버킷 공개 액세스 차단 설정 +resource "aws_s3_bucket_public_access_block" "this" { + count = var.enable_block_public_access ? 1 : 0 + bucket = aws_s3_bucket.this.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# S3 버킷 공개 읽기 정책 (GET만 허용) +resource "aws_s3_bucket_policy" "public_read" { + count = var.enable_public_read ? 1 : 0 + bucket = aws_s3_bucket.this.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = "*" + Action = [ + "s3:GetObject" + ] + Resource = "${aws_s3_bucket.this.arn}/*" + } + ] + }) + + depends_on = [aws_s3_bucket_public_access_block.this] +} + +# S3 버킷 수명 주기 정책 (선택적) +resource "aws_s3_bucket_lifecycle_configuration" "this" { + count = var.enable_lifecycle_policy && length(var.lifecycle_rules) > 0 ? 1 : 0 + bucket = aws_s3_bucket.this.id + + dynamic "rule" { + for_each = var.lifecycle_rules + content { + id = rule.value.id + status = rule.value.status + + dynamic "transition" { + for_each = rule.value.transitions + content { + days = transition.value.days + storage_class = transition.value.storage_class + } + } + + dynamic "expiration" { + for_each = rule.value.expiration != null ? [rule.value.expiration] : [] + content { + days = expiration.value.days + } + } + } + } +} diff --git a/terraform/modules/storage/s3/output.tf b/terraform/modules/storage/s3/output.tf new file mode 100644 index 0000000..3b93924 --- /dev/null +++ b/terraform/modules/storage/s3/output.tf @@ -0,0 +1,40 @@ +output "bucket_arn" { + description = "The ARN of the bucket" + value = aws_s3_bucket.this.arn +} + +output "bucket_name" { + description = "The name of the bucket" + value = aws_s3_bucket.this.id +} + +output "bucket_id" { + description = "The name of the bucket" + value = aws_s3_bucket.this.id +} + +output "bucket_domain_name" { + description = "The bucket domain name" + value = aws_s3_bucket.this.bucket_domain_name +} + +output "bucket_regional_domain_name" { + description = "The bucket region-specific domain name" + value = aws_s3_bucket.this.bucket_regional_domain_name +} + +output "bucket_region" { + description = "The AWS region this bucket resides in" + value = aws_s3_bucket.this.region +} + +output "bucket_website_endpoint" { + description = "The website endpoint of the bucket" + value = aws_s3_bucket.this.website_domain + # Note: using website_domain instead of deprecated website_endpoint +} + +output "bucket_public_url" { + description = "The public URL for accessing objects in the bucket" + value = "https://${aws_s3_bucket.this.bucket_domain_name}" +} diff --git a/terraform/modules/storage/s3/variables.tf b/terraform/modules/storage/s3/variables.tf new file mode 100644 index 0000000..e5a4b23 --- /dev/null +++ b/terraform/modules/storage/s3/variables.tf @@ -0,0 +1,76 @@ +variable "bucket_name" { + description = "S3 bucket name" + type = string +} + +variable "environment" { + description = "Environment name (ex: dev or prod)" + type = string +} + +variable "purpose" { + description = "Usage purpose (ex: tfstate, image, backup)" + type = string +} + +variable "tags" { + description = "Additional tags for the bucket" + type = map(string) + default = {} +} + +variable "enable_versioning" { + description = "Whether version management is enabled" + type = bool + default = false +} + +variable "enable_sse" { + description = "Whether server-side encryption is enabled" + type = bool + default = false +} + +variable "sse_algorithm" { + description = "Server-side encryption algorithm (AES256 or aws:kms)" + type = string + default = "AES256" + validation { + condition = contains(["AES256", "aws:kms"], var.sse_algorithm) + error_message = "SSE algorithm must be either AES256 or aws:kms." + } +} + +variable "enable_block_public_access" { + description = "Whether to block public access to the bucket" + type = bool + default = true +} + +variable "enable_public_read" { + description = "Whether to enable public read access (GET only)" + type = bool + default = false +} + +variable "enable_lifecycle_policy" { + description = "Whether to enable lifecycle policy" + type = bool + default = false +} + +variable "lifecycle_rules" { + description = "List of lifecycle rules for the bucket" + type = list(object({ + id = string + status = string + transitions = list(object({ + days = number + storage_class = string + })) + expiration = optional(object({ + days = number + })) + })) + default = [] +} \ No newline at end of file diff --git a/userdata-examples/was-userdata.sh b/userdata-examples/was-userdata.sh new file mode 100644 index 0000000..afd8929 --- /dev/null +++ b/userdata-examples/was-userdata.sh @@ -0,0 +1,296 @@ +#!/bin/bash + +# UserData 스크립트 실행 권한 확인 및 설정 +chmod +x "$0" + +# 로그 파일 설정 +LOG_FILE="/var/log/userdata.log" +exec > >(tee -a $LOG_FILE) 2>&1 + +echo "==========================================" +echo "UserData 스크립트 시작: $(date)" +echo "현재 사용자: $(whoami)" +echo "현재 디렉토리: $(pwd)" +echo "환경 변수 PATH: $PATH" +echo "Nginx 설정 파일 경로: /etc/nginx/nginx.conf" +echo "Nginx 설정 파일 존재 여부: $(ls -la /etc/nginx/nginx.conf 2>/dev/null || echo '파일 없음')" +echo "==========================================" + +# 시스템 업데이트 +echo "시스템 업데이트 시작..." +sudo apt update -y +if [ $? -eq 0 ]; then + echo "✅ 패키지 목록 업데이트 완료" +else + echo "❌ 패키지 목록 업데이트 실패" +fi + +sudo apt upgrade -y +if [ $? -eq 0 ]; then + echo "✅ 시스템 업그레이드 완료" +else + echo "❌ 시스템 업그레이드 실패" +fi + +# Java 21 설치 +echo "Java 21 설치 중..." +sudo apt install -y openjdk-21-jdk +if [ $? -eq 0 ]; then + echo "✅ Java 21 설치 완료" +else + echo "❌ Java 21 설치 실패" +fi + +# MySQL 클라이언트 설치 +sudo apt install mysql-client-core-8.0 + +# Docker 설치 +echo "Docker 설치 중..." +sudo apt install -y docker.io +if [ $? -eq 0 ]; then + echo "✅ Docker 설치 완료" +else + echo "❌ Docker 설치 실패" +fi + +# Docker 서비스 시작 및 자동 시작 설정 +echo "Docker 서비스 시작 중..." +sudo systemctl start docker +sudo systemctl enable docker +if [ $? -eq 0 ]; then + echo "✅ Docker 서비스 시작 완료" +else + echo "❌ Docker 서비스 시작 실패" +fi + +# Docker Compose v2 설치 +echo "Docker Compose v2 설치 중..." +mkdir -p ~/.docker/cli-plugins/ +curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose +chmod +x ~/.docker/cli-plugins/docker-compose +if [ $? -eq 0 ]; then + echo "✅ Docker Compose v2 설치 완료" +else + echo "❌ Docker Compose v2 설치 실패" +fi + +# Nginx 설치 +echo "Nginx 설치 중..." +sudo apt install -y nginx +if [ $? -eq 0 ]; then + echo "✅ Nginx 설치 완료" +else + echo "❌ Nginx 설치 실패" +fi + +# Nginx 서비스 시작 및 자동 시작 설정 +echo "Nginx 서비스 시작 중..." +sudo systemctl start nginx +sudo systemctl enable nginx +if [ $? -eq 0 ]; then + echo "✅ Nginx 서비스 시작 완료" +else + echo "❌ Nginx 서비스 시작 실패" +fi + +# Nginx 설정 파일 백업 +echo "Nginx 설정 파일 백업 중..." +sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup +if [ $? -eq 0 ]; then + echo "✅ Nginx 설정 파일 백업 완료" +else + echo "❌ Nginx 설정 파일 백업 실패" +fi + +# Nginx 설정 파일 생성 +echo "Nginx 설정 파일 생성 중..." +sudo tee /etc/nginx/nginx.conf > /dev/null << 'EOF' +user www-data; +worker_processes auto; +pid /run/nginx.pid; +error_log /var/log/nginx/error.log; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + server { + listen 80; + server_name clokey.shop; + location / { + proxy_pass http://localhost:8080; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } + + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + + ## + # Gzip Settings + ## + + gzip on; + + # gzip_vary on; + # gzip_proxied any; + # gzip_comp_level 6; + # gzip_buffers 16 8k; + # gzip_http_version 1.1; + # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + ## + # Virtual Host Configs + ## + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; + + client_max_body_size 100M; +} +EOF + +if [ $? -eq 0 ]; then + echo "✅ Nginx 설정 파일 생성 완료" +else + echo "❌ Nginx 설정 파일 생성 실패" +fi + +# Nginx 설정 테스트 +echo "Nginx 설정 테스트 중..." +sudo nginx -t +if [ $? -eq 0 ]; then + echo "✅ Nginx 설정 테스트 성공" + # Nginx 재시작 + sudo systemctl restart nginx + if [ $? -eq 0 ]; then + echo "✅ Nginx 재시작 완료" + else + echo "❌ Nginx 재시작 실패" + fi +else + echo "❌ Nginx 설정 테스트 실패" + # 백업 파일로 복원 + sudo cp /etc/nginx/nginx.conf.backup /etc/nginx/nginx.conf + sudo systemctl restart nginx +fi + +# Swap 메모리 설정 (2GB) +echo "Swap 메모리 설정 중..." +sudo fallocate -l 2G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +if [ $? -eq 0 ]; then + echo "✅ Swap 메모리 설정 완료" +else + echo "❌ Swap 메모리 설정 실패" +fi + +# Swap 영구 설정 +echo '/swapfile none swap sw 0 0' >> /etc/fstab + +# Swap 설정 확인 +echo "Swap 설정 완료:" +swapon --show + +# Redis 컨테이너 실행 +echo "Redis 컨테이너 시작 중..." +sudo docker run -d \ + --name redis-container \ + --restart unless-stopped \ + -p 6379:6379 \ + redis:latest + +# Redis 컨테이너 상태 확인 +echo "Redis 컨테이너 상태 확인 중..." +sleep 5 +if sudo docker ps | grep -q redis-container; then + echo "✅ Redis 컨테이너가 성공적으로 시작되었습니다." +else + echo "❌ Redis 컨테이너 시작에 실패했습니다." +fi + +# EC2 재시작 시 자동 설정을 위한 systemd 서비스 생성 +echo "자동 재시작 서비스 설정 중..." +cat > /etc/systemd/system/clokey-setup.service << 'EOF' +[Unit] +Description=Clokey Application Setup +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/bash -c ' + # Swap 활성화 (이미 설정되어 있으면 무시) + if ! swapon --show | grep -q /swapfile; then + swapon /swapfile + fi + + # Redis 컨테이너 시작 (이미 실행 중이면 무시) + if ! docker ps --format "table {{.Names}}" | grep -q redis-container; then + docker start redis-container 2>/dev/null || docker run -d --name redis-container --restart unless-stopped -p 6379:6379 redis:latest + fi +' + +[Install] +WantedBy=multi-user.target +EOF + +# 서비스 활성화 +systemctl daemon-reload +systemctl enable clokey-setup.service +if [ $? -eq 0 ]; then + echo "✅ 자동 재시작 서비스 활성화 완료" +else + echo "❌ 자동 재시작 서비스 활성화 실패" +fi + +# 설치 완료 메시지 +echo "==========================================" +echo "WAS 서버 초기 설정 완료!" +echo "Java 버전: $(java -version 2>&1 | head -n 1)" +echo "Docker 버전: $(docker --version)" +echo "Docker Compose 버전: $(docker compose version)" +echo "Nginx 버전: $(nginx -v 2>&1)" +echo "Nginx 상태: $(systemctl is-active nginx)" +echo "Swap 메모리: $(swapon --show | grep /swapfile | awk '{print $3}')" +echo "Redis 컨테이너 상태: $(docker ps --filter name=redis-container --format 'table {{.Status}}')" +echo "자동 재시작 서비스: $(systemctl is-enabled clokey-setup.service)" +echo "==========================================" +echo "UserData 스크립트 완료: $(date)" +echo "로그 파일 위치: $LOG_FILE" +echo "=========================================="