diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..384a507 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,18 @@ +language: ko-KR + +reviews: + auto_review: + enabled: false + + path_instructions: + - path: "**/*.tf" + instructions: | + 이 PR의 Terraform 코드 변경을 리뷰합니다. + PR 댓글에 올라온 각 환경의 "Terraform Plan" 결과를 반드시 확인하고, 코드 변경과 plan 결과가 일치하는지 검토하세요. + + 다음 항목을 중점적으로 검토하세요: + - plan에 예상치 못한 resource destroy 또는 replace가 포함된 경우 + - 보안 그룹(Security Group) 인바운드/아웃바운드 규칙 변경 + - IAM 권한이 과도하게 부여된 경우 (최소 권한 원칙) + - 민감한 값(비밀번호, 키 등)이 코드에 하드코딩된 경우 + - `lifecycle.ignore_changes` 설정이 의도에 맞게 사용되었는지 diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml new file mode 100644 index 0000000..c197776 --- /dev/null +++ b/.github/workflows/terraform-apply.yml @@ -0,0 +1,269 @@ +name: Terraform Apply + +on: + push: + branches: [main] + +permissions: + id-token: write + contents: read + +env: + TF_VERSION: "1.10.5" + SSM_TUNNEL_TIMEOUT: "60" + +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + bootstrap: ${{ steps.filter.outputs.bootstrap }} + global: ${{ steps.filter.outputs.global }} + prod: ${{ steps.filter.outputs.prod }} + stage: ${{ steps.filter.outputs.stage }} + monitoring: ${{ steps.filter.outputs.monitoring }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + bootstrap: + - 'bootstrap/**' + global: + - 'environment/global/**' + - 'modules/shared_resources/**' + - 'config/secrets/shared_resources.tfvars' + prod: + - 'environment/prod/**' + - 'modules/app_stack/**' + - 'modules/common/**' + - 'config/secrets/prod.tfvars' + - 'config/secrets/app_stack.tfvars' + stage: + - 'environment/stage/**' + - 'modules/app_stack/**' + - 'modules/common/**' + - 'config/secrets/stage.tfvars' + - 'config/secrets/app_stack.tfvars' + monitoring: + - 'environment/monitoring/**' + - 'modules/monitoring_stack/**' + - 'modules/common/**' + - 'config/secrets/monitoring.tfvars' + - 'config/secrets/monitoring_stack.tfvars' + + apply-bootstrap: + needs: detect-changes + if: needs.detect-changes.outputs.bootstrap == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: bootstrap + run: terraform init + - name: Terraform Apply + working-directory: bootstrap + run: terraform apply -auto-approve + + apply-global: + needs: [detect-changes, apply-bootstrap] + if: | + always() && + needs.detect-changes.outputs.global == 'true' && + (needs.apply-bootstrap.result == 'success' || needs.apply-bootstrap.result == 'skipped') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/global + run: terraform init + - name: Terraform Apply + working-directory: environment/global + run: | + terraform apply -auto-approve \ + -var-file="../../config/secrets/shared_resources.tfvars" + + apply-prod: + needs: [detect-changes, apply-bootstrap] + if: | + always() && + needs.detect-changes.outputs.prod == 'true' && + (needs.apply-bootstrap.result == 'success' || needs.apply-bootstrap.result == 'skipped') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Install Session Manager Plugin + run: | + curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ + -o /tmp/session-manager-plugin.deb + sudo dpkg -i /tmp/session-manager-plugin.deb + echo "/usr/local/sessionmanagerplugin/bin" >> $GITHUB_PATH + /usr/local/sessionmanagerplugin/bin/session-manager-plugin --version + - name: Start SSM Tunnel to RDS + run: | + echo "=== session-manager-plugin 진단 ===" + which session-manager-plugin || echo "NOT IN PATH" + session-manager-plugin --version || echo "VERSION CHECK FAILED" + echo "====================================" + + EC2_ID=$(aws ec2 describe-instances \ + --filters "Name=tag:Name,Values=solid-connection-server-prod" "Name=instance-state-name,Values=running" \ + --query 'Reservations[0].Instances[0].InstanceId' \ + --output text) + + RDS_HOST=$(aws rds describe-db-instances \ + --query 'DBInstances[?contains(DBInstanceIdentifier, `prod`)].Endpoint.Address | [0]' \ + --output text) + + if [ -z "$EC2_ID" ] || [ "$EC2_ID" = "None" ]; then + echo "::error::prod EC2 인스턴스를 찾을 수 없습니다" + exit 1 + fi + if [ -z "$RDS_HOST" ] || [ "$RDS_HOST" = "None" ]; then + echo "::error::prod RDS 엔드포인트를 찾을 수 없습니다" + exit 1 + fi + + echo "Tunneling via $EC2_ID -> $RDS_HOST:3306" + + aws ssm start-session \ + --target "$EC2_ID" \ + --document-name AWS-StartPortForwardingSessionToRemoteHost \ + --parameters "{\"host\":[\"$RDS_HOST\"],\"portNumber\":[\"3306\"],\"localPortNumber\":[\"3306\"]}" & + SSM_PID=$! + echo "SSM_PID=$SSM_PID" >> $GITHUB_ENV + + for i in $(seq 1 $SSM_TUNNEL_TIMEOUT); do + if ! kill -0 $SSM_PID 2>/dev/null; then + echo "::error::SSM 세션이 터널 준비 전에 종료되었습니다" + exit 1 + fi + if nc -z 127.0.0.1 3306 2>/dev/null; then + echo "SSM tunnel ready (${i}s)" + break + fi + sleep 1 + done + + if ! nc -z 127.0.0.1 3306 2>/dev/null; then + echo "::error::${SSM_TUNNEL_TIMEOUT}초 내에 터널이 준비되지 않았습니다" + kill $SSM_PID 2>/dev/null || true + exit 1 + fi + if ! kill -0 $SSM_PID 2>/dev/null; then + echo "::error::포트는 열렸으나 SSM 세션이 이미 종료되었습니다" + exit 1 + fi + - name: Terraform Init + working-directory: environment/prod + run: terraform init + - name: Terraform Apply + working-directory: environment/prod + run: | + terraform apply -auto-approve \ + -var-file="../../config/secrets/prod.tfvars" \ + -var-file="../../config/secrets/app_stack.tfvars" + - name: Stop SSM Tunnel + if: always() + run: kill $SSM_PID 2>/dev/null || true + + apply-stage: + needs: [detect-changes, apply-bootstrap] + if: | + always() && + needs.detect-changes.outputs.stage == 'true' && + (needs.apply-bootstrap.result == 'success' || needs.apply-bootstrap.result == 'skipped') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/stage + run: terraform init + - name: Terraform Apply + working-directory: environment/stage + run: | + terraform apply -auto-approve \ + -var-file="../../config/secrets/stage.tfvars" \ + -var-file="../../config/secrets/app_stack.tfvars" + + apply-monitoring: + needs: [detect-changes, apply-bootstrap] + if: | + always() && + needs.detect-changes.outputs.monitoring == 'true' && + (needs.apply-bootstrap.result == 'success' || needs.apply-bootstrap.result == 'skipped') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/monitoring + run: terraform init + - name: Terraform Apply + working-directory: environment/monitoring + run: | + terraform apply -auto-approve \ + -var-file="../../config/secrets/monitoring.tfvars" \ + -var-file="../../config/secrets/monitoring_stack.tfvars" diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml new file mode 100644 index 0000000..870f6e3 --- /dev/null +++ b/.github/workflows/terraform-plan.yml @@ -0,0 +1,479 @@ +name: Terraform Plan + +on: + pull_request: + branches: [main] + +permissions: + id-token: write + contents: read + pull-requests: write + +env: + TF_VERSION: "1.10.5" + SSM_TUNNEL_TIMEOUT: "60" + +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + bootstrap: ${{ steps.filter.outputs.bootstrap }} + global: ${{ steps.filter.outputs.global }} + prod: ${{ steps.filter.outputs.prod }} + stage: ${{ steps.filter.outputs.stage }} + monitoring: ${{ steps.filter.outputs.monitoring }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + bootstrap: + - 'bootstrap/**' + global: + - 'environment/global/**' + - 'modules/shared_resources/**' + - 'config/secrets/shared_resources.tfvars' + prod: + - 'environment/prod/**' + - 'modules/app_stack/**' + - 'modules/common/**' + - 'config/secrets/prod.tfvars' + - 'config/secrets/app_stack.tfvars' + stage: + - 'environment/stage/**' + - 'modules/app_stack/**' + - 'modules/common/**' + - 'config/secrets/stage.tfvars' + - 'config/secrets/app_stack.tfvars' + monitoring: + - 'environment/monitoring/**' + - 'modules/monitoring_stack/**' + - 'modules/common/**' + - 'config/secrets/monitoring.tfvars' + - 'config/secrets/monitoring_stack.tfvars' + + plan-bootstrap: + needs: detect-changes + if: needs.detect-changes.outputs.bootstrap == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: bootstrap + run: terraform init + - name: Terraform Plan + id: plan + working-directory: bootstrap + run: | + terraform plan -no-color 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-bootstrap + path: bootstrap/plan_output.txt + - name: Post Plan Comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const planFile = 'bootstrap/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`bootstrap\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 + + plan-global: + needs: detect-changes + if: needs.detect-changes.outputs.global == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/global + run: terraform init + - name: Terraform Plan + id: plan + working-directory: environment/global + run: | + terraform plan -no-color \ + -var-file="../../config/secrets/shared_resources.tfvars" \ + 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-global + path: environment/global/plan_output.txt + - name: Post Plan Comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const planFile = 'environment/global/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`global\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 + + plan-prod: + needs: detect-changes + if: needs.detect-changes.outputs.prod == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Install Session Manager Plugin + run: | + curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ + -o /tmp/session-manager-plugin.deb + sudo dpkg -i /tmp/session-manager-plugin.deb + # 이후 모든 스텝의 PATH 맨 앞에 신규 설치 경로 추가 + echo "/usr/local/sessionmanagerplugin/bin" >> $GITHUB_PATH + /usr/local/sessionmanagerplugin/bin/session-manager-plugin --version + - name: Start SSM Tunnel to RDS + run: | + echo "=== session-manager-plugin 진단 ===" + which session-manager-plugin || echo "NOT IN PATH" + session-manager-plugin --version || echo "VERSION CHECK FAILED" + echo "====================================" + + EC2_ID=$(aws ec2 describe-instances \ + --filters "Name=tag:Name,Values=solid-connection-server-prod" "Name=instance-state-name,Values=running" \ + --query 'Reservations[0].Instances[0].InstanceId' \ + --output text) + + RDS_HOST=$(aws rds describe-db-instances \ + --query 'DBInstances[?contains(DBInstanceIdentifier, `prod`)].Endpoint.Address | [0]' \ + --output text) + + if [ -z "$EC2_ID" ] || [ "$EC2_ID" = "None" ]; then + echo "::error::prod EC2 인스턴스를 찾을 수 없습니다" + exit 1 + fi + if [ -z "$RDS_HOST" ] || [ "$RDS_HOST" = "None" ]; then + echo "::error::prod RDS 엔드포인트를 찾을 수 없습니다" + exit 1 + fi + + echo "Tunneling via $EC2_ID -> $RDS_HOST:3306" + + aws ssm start-session \ + --target "$EC2_ID" \ + --document-name AWS-StartPortForwardingSessionToRemoteHost \ + --parameters "{\"host\":[\"$RDS_HOST\"],\"portNumber\":[\"3306\"],\"localPortNumber\":[\"3306\"]}" & + SSM_PID=$! + echo "SSM_PID=$SSM_PID" >> $GITHUB_ENV + + for i in $(seq 1 $SSM_TUNNEL_TIMEOUT); do + if ! kill -0 $SSM_PID 2>/dev/null; then + echo "::error::SSM 세션이 터널 준비 전에 종료되었습니다" + exit 1 + fi + if nc -z 127.0.0.1 3306 2>/dev/null; then + echo "SSM tunnel ready (${i}s)" + break + fi + sleep 1 + done + + if ! nc -z 127.0.0.1 3306 2>/dev/null; then + echo "::error::${SSM_TUNNEL_TIMEOUT}초 내에 터널이 준비되지 않았습니다" + kill $SSM_PID 2>/dev/null || true + exit 1 + fi + if ! kill -0 $SSM_PID 2>/dev/null; then + echo "::error::포트는 열렸으나 SSM 세션이 이미 종료되었습니다" + exit 1 + fi + - name: Terraform Init + working-directory: environment/prod + run: terraform init + - name: Terraform Plan + id: plan + working-directory: environment/prod + run: | + terraform plan -no-color \ + -var-file="../../config/secrets/prod.tfvars" \ + -var-file="../../config/secrets/app_stack.tfvars" \ + 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Stop SSM Tunnel + if: always() + run: kill $SSM_PID 2>/dev/null || true + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-prod + path: environment/prod/plan_output.txt + - name: Post Plan Comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const planFile = 'environment/prod/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`prod\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 + + plan-stage: + needs: detect-changes + if: needs.detect-changes.outputs.stage == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/stage + run: terraform init + - name: Terraform Plan + id: plan + working-directory: environment/stage + run: | + terraform plan -no-color \ + -var-file="../../config/secrets/stage.tfvars" \ + -var-file="../../config/secrets/app_stack.tfvars" \ + 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-stage + path: environment/stage/plan_output.txt + - name: Post Plan Comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const planFile = 'environment/stage/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`stage\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 + + plan-monitoring: + needs: detect-changes + if: needs.detect-changes.outputs.monitoring == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/monitoring + run: terraform init + - name: Terraform Plan + id: plan + working-directory: environment/monitoring + run: | + terraform plan -no-color \ + -var-file="../../config/secrets/monitoring.tfvars" \ + -var-file="../../config/secrets/monitoring_stack.tfvars" \ + 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-monitoring + path: environment/monitoring/plan_output.txt + - name: Post Plan Comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const planFile = 'environment/monitoring/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`monitoring\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 + + trigger-coderabbit: + needs: [plan-bootstrap, plan-global, plan-prod, plan-stage, plan-monitoring] + if: | + always() && + ( + needs.plan-bootstrap.result == 'success' || needs.plan-bootstrap.result == 'failure' || + needs.plan-global.result == 'success' || needs.plan-global.result == 'failure' || + needs.plan-prod.result == 'success' || needs.plan-prod.result == 'failure' || + needs.plan-stage.result == 'success' || needs.plan-stage.result == 'failure' || + needs.plan-monitoring.result == 'success' || needs.plan-monitoring.result == 'failure' + ) + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const marker = ''; + const body = `${marker}\n@coderabbitai review`; + + const { data: comments } = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + comment_id: existing.id, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); + } else { + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); + } diff --git a/bootstrap/iam.tf b/bootstrap/iam.tf new file mode 100644 index 0000000..5b8901f --- /dev/null +++ b/bootstrap/iam.tf @@ -0,0 +1,125 @@ +data "aws_caller_identity" "current" {} + +# ============================================= +# EC2 공유 IAM Role에 SSM 정책 부착 +# ============================================= + +data "aws_iam_role" "ec2_shared" { + name = "SolidConnectionParameterStoreReadRole" +} + +resource "aws_iam_role_policy_attachment" "ec2_ssm" { + role = data.aws_iam_role.ec2_shared.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" +} + +# ============================================= +# 개발자용 IAM Policy (수동 관리, Terraform은 참조만) +# ============================================= + +data "aws_iam_policy" "developer_tfstate" { + name = "TerraformStateAccessPolicy" +} + +# ============================================= +# GitHub Actions OIDC +# ============================================= + +resource "aws_iam_openid_connect_provider" "github" { + url = "https://token.actions.githubusercontent.com" + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1", "1c58a3a8518e8759bf075b76b750d4f2df264fcd"] + tags = { + Project = "solid-connection" + Env = "bootstrap" + } +} + +# Apply Role: main 브랜치 push 전용 (terraform apply) +resource "aws_iam_role" "github_actions" { + name = "GitHubActionsTerraformRole" + description = "IAM Role for GitHub Actions terraform apply via OIDC (main only)" + tags = { + Project = "solid-connection" + Env = "bootstrap" + } + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { Federated = aws_iam_openid_connect_provider.github.arn } + Action = "sts:AssumeRoleWithWebIdentity" + Condition = { + StringEquals = { + "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" + } + StringLike = { + "token.actions.githubusercontent.com:sub" = "repo:solid-connection/solid-connection-infra:ref:refs/heads/main" + } + } + }] + }) +} + +# Plan Role: PR 전용 (terraform plan, 읽기 전용) +resource "aws_iam_role" "github_actions_plan" { + name = "GitHubActionsTerraformPlanRole" + description = "IAM Role for GitHub Actions terraform plan via OIDC (pull_request only)" + tags = { + Project = "solid-connection" + Env = "bootstrap" + } + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { Federated = aws_iam_openid_connect_provider.github.arn } + Action = "sts:AssumeRoleWithWebIdentity" + Condition = { + StringEquals = { + "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" + } + StringLike = { + "token.actions.githubusercontent.com:sub" = "repo:solid-connection/solid-connection-infra:pull_request" + } + } + }] + }) +} + +# GitHub Actions 정책 (수동 관리, Terraform은 참조만) +data "aws_iam_policy" "github_actions_tfstate" { + name = "GitHubActionsTfstatePolicy" +} + +data "aws_iam_policy" "github_actions_infra" { + name = "GitHubActionsTerraformInfraPolicy" +} + +data "aws_iam_policy" "github_actions_infra_read" { + name = "GithubActionsTerraformInfraReadPolicy" +} + +# Apply Role 정책 연결 +resource "aws_iam_role_policy_attachment" "github_actions_tfstate" { + role = aws_iam_role.github_actions.name + policy_arn = data.aws_iam_policy.github_actions_tfstate.arn +} + +resource "aws_iam_role_policy_attachment" "github_actions_infra" { + role = aws_iam_role.github_actions.name + policy_arn = data.aws_iam_policy.github_actions_infra.arn +} + +# Plan Role 정책 연결 (tfstate 읽기 전용 + 인프라 읽기 전용) +resource "aws_iam_role_policy_attachment" "github_actions_plan_tfstate" { + role = aws_iam_role.github_actions_plan.name + policy_arn = data.aws_iam_policy.developer_tfstate.arn +} + +resource "aws_iam_role_policy_attachment" "github_actions_plan_infra_read" { + role = aws_iam_role.github_actions_plan.name + policy_arn = data.aws_iam_policy.github_actions_infra_read.arn +} diff --git a/bootstrap/outputs.tf b/bootstrap/outputs.tf new file mode 100644 index 0000000..538299d --- /dev/null +++ b/bootstrap/outputs.tf @@ -0,0 +1,22 @@ +output "tfstate_bucket_arn" { + value = aws_s3_bucket.tfstate.arn +} + +output "tfstate_bucket_name" { + value = aws_s3_bucket.tfstate.bucket +} + +output "developer_tfstate_policy_arn" { + description = "개발자 IAM 유저에 attach할 tfstate 접근 Policy ARN" + value = data.aws_iam_policy.developer_tfstate.arn +} + +output "github_actions_role_arn" { + description = "GitHub Actions apply workflow에서 사용할 IAM Role ARN (main 전용)" + value = aws_iam_role.github_actions.arn +} + +output "github_actions_plan_role_arn" { + description = "GitHub Actions plan workflow에서 사용할 IAM Role ARN (PR 전용)" + value = aws_iam_role.github_actions_plan.arn +} diff --git a/bootstrap/provider.tf b/bootstrap/provider.tf new file mode 100644 index 0000000..eec93f5 --- /dev/null +++ b/bootstrap/provider.tf @@ -0,0 +1,30 @@ +terraform { + required_version = ">= 1.10.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/bootstrap/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } +} + +provider "aws" { + region = "ap-northeast-2" + + default_tags { + tags = { + Env = "bootstrap" + Project = "solid-connection" + ManagedBy = "terraform" + } + } +} diff --git a/bootstrap/s3.tf b/bootstrap/s3.tf new file mode 100644 index 0000000..09159fc --- /dev/null +++ b/bootstrap/s3.tf @@ -0,0 +1,56 @@ +resource "aws_s3_bucket" "tfstate" { + bucket = "solid-connection-tfstate" + + lifecycle { + prevent_destroy = true + } +} + +resource "aws_s3_bucket_versioning" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_policy" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.tfstate.arn, + "${aws_s3_bucket.tfstate.arn}/*" + ] + Condition = { + Bool = { "aws:SecureTransport" = "false" } + } + }] + }) + + depends_on = [aws_s3_bucket_public_access_block.tfstate] +} diff --git a/config/secrets b/config/secrets index 8e6a967..f88a84c 160000 --- a/config/secrets +++ b/config/secrets @@ -1 +1 @@ -Subproject commit 8e6a96778a2977516ad0e7210fa7f24561b8f3e1 +Subproject commit f88a84cdab72136d294614fd1e2c855c4a026c43 diff --git a/environment/global/main.tf b/environment/global/main.tf index ea785f0..5701eb6 100644 --- a/environment/global/main.tf +++ b/environment/global/main.tf @@ -5,7 +5,6 @@ module "shared_resources" { aws = aws } - s3_default_bucket_name = var.s3_default_bucket_name s3_upload_bucket_name = var.s3_upload_bucket_name resizing_img_func_name = var.resizing_img_func_name @@ -20,6 +19,5 @@ module "shared_resources" { thumbnail_generating_func_runtime = var.thumbnail_generating_func_runtime thumbnail_generating_func_layers = var.thumbnail_generating_func_layers - default_cdn_web_acl_id = var.default_cdn_web_acl_id upload_cdn_web_acl_id = var.upload_cdn_web_acl_id -} \ No newline at end of file +} diff --git a/environment/global/provider.tf b/environment/global/provider.tf index 38fdf1e..fe58799 100644 --- a/environment/global/provider.tf +++ b/environment/global/provider.tf @@ -1,12 +1,20 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.10.0" required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.0" + version = ">= 5.0" } } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/global/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } } provider "aws" { diff --git a/environment/global/variables.tf b/environment/global/variables.tf index 58d89e5..b994d24 100644 --- a/environment/global/variables.tf +++ b/environment/global/variables.tf @@ -1,9 +1,4 @@ # [S3 버킷 관련 변수] -variable "s3_default_bucket_name" { - description = "Name of the default S3 bucket" - type = string -} - variable "s3_upload_bucket_name" { description = "Name of the upload S3 bucket" type = string @@ -60,11 +55,6 @@ variable "thumbnail_generating_func_layers" { type = list(string) } -variable "default_cdn_web_acl_id" { - description = "WAF Web ACL Id for Default Cloudfront CDN" - type = string -} - variable "upload_cdn_web_acl_id" { description = "WAF Web ACL Id for Upload Cloudfront CDN" type = string diff --git a/environment/monitoring/provider.tf b/environment/monitoring/provider.tf index 3c04703..08141c4 100644 --- a/environment/monitoring/provider.tf +++ b/environment/monitoring/provider.tf @@ -1,3 +1,26 @@ +terraform { + required_version = ">= 1.10.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + cloudinit = { + source = "hashicorp/cloudinit" + version = "~> 2.3" + } + } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/monitoring/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } +} + provider "aws" { region = "ap-northeast-2" default_tags { diff --git a/environment/prod/main.tf b/environment/prod/main.tf index 16d584d..320f5e4 100644 --- a/environment/prod/main.tf +++ b/environment/prod/main.tf @@ -11,6 +11,9 @@ module "prod_stack" { ami_id = var.ami_id + # IAM Instance Profile (SSM + Parameter Store 접근) + ec2_iam_instance_profile = var.ec2_iam_instance_profile + # 키페어 및 접속 허용 key_name = var.key_name diff --git a/environment/prod/provider.tf b/environment/prod/provider.tf index 087653c..52f4aec 100644 --- a/environment/prod/provider.tf +++ b/environment/prod/provider.tf @@ -1,3 +1,26 @@ +terraform { + required_version = ">= 1.10.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + mysql = { + source = "petoju/mysql" + version = ">= 3.0" + } + } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/prod/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } +} + provider "aws" { region = "ap-northeast-2" default_tags { @@ -7,3 +30,10 @@ provider "aws" { } } } + +# MySQL Provider 설정 (SSH 터널링을 통해 로컬호스트로 접속) +provider "mysql" { + endpoint = "127.0.0.1:3306" + username = var.db_root_username + password = var.db_root_password +} diff --git a/environment/prod/variables.tf b/environment/prod/variables.tf index 324438a..0a74365 100644 --- a/environment/prod/variables.tf +++ b/environment/prod/variables.tf @@ -1,3 +1,8 @@ +variable "ec2_iam_instance_profile" { + description = "EC2에 연결할 IAM Instance Profile 이름" + type = string +} + variable "ami_id" { description = "AMI ID for the prod environment" type = string diff --git a/environment/stage/main.tf b/environment/stage/main.tf index f5c999e..3f3e129 100644 --- a/environment/stage/main.tf +++ b/environment/stage/main.tf @@ -11,42 +11,31 @@ module "stage_stack" { ami_id = var.ami_id + # IAM Instance Profile (SSM + Parameter Store 접근) + ec2_iam_instance_profile = var.ec2_iam_instance_profile + # 키페어 및 접속 허용 key_name = var.key_name - + # 인스턴스 스펙 - instance_type = var.server_instance_type - db_instance_class = var.db_instance_class + instance_type = var.server_instance_type + + # RDS 미사용 (Docker container로 대체) + enable_rds = false # 보안 그룹 규칙 api_ingress_rules = var.api_ingress_rules - db_ingress_rules = var.db_ingress_rules - - # RDS 식별자 설정 - rds_identifier = var.rds_identifier - - # DB 계정 정보 - db_username = var.db_root_username - db_password = var.db_root_password - - # DB 엔진 및 암호화 설정 - db_engine_version = var.db_engine_version # MySQL 버전 지정 - db_parameter_group_name = var.db_parameter_group_name # MySQL 파라미터 그룹 지정 - kms_key_arn = var.kms_key_arn # KMS ARN 변수 전달 - - # 추가 유저마다 다른 권한 부여 - additional_db_users = var.additional_db_users # Nginx 및 도메인 설정 - domain_name = var.domain_name - cert_email = var.cert_email + domain_name = var.domain_name + cert_email = var.cert_email nginx_conf_name = var.nginx_conf_name # ssh key 경로 전달 ssh_key_path = var.ssh_key_path # Side Infra 관련 변수 전달 - work_dir = var.work_dir + work_dir = var.work_dir alloy_env_name = var.alloy_env_name redis_version = var.redis_version diff --git a/environment/stage/provider.tf b/environment/stage/provider.tf index 87dc788..3f6b867 100644 --- a/environment/stage/provider.tf +++ b/environment/stage/provider.tf @@ -1,3 +1,22 @@ +terraform { + required_version = ">= 1.10.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/stage/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } +} + provider "aws" { region = "ap-northeast-2" default_tags { diff --git a/environment/stage/variables.tf b/environment/stage/variables.tf index 5245959..e432b29 100644 --- a/environment/stage/variables.tf +++ b/environment/stage/variables.tf @@ -1,3 +1,8 @@ +variable "ec2_iam_instance_profile" { + description = "EC2에 연결할 IAM Instance Profile 이름" + type = string +} + variable "ami_id" { description = "AMI ID for the stage environment" type = string @@ -8,11 +13,6 @@ variable "server_instance_type" { type = string } -variable "db_instance_class" { - description = "DB instance class for the stage environment" - type = string -} - variable "api_ingress_rules" { description = "List of ingress rules for API Server" type = list(object({ @@ -24,61 +24,11 @@ variable "api_ingress_rules" { })) } -variable "db_ingress_rules" { - description = "List of ingress rules for DB Server" - type = list(object({ - from_port = number - to_port = number - protocol = string - description = string - })) -} - -variable "rds_identifier" { - description = "RDS identifier for the stage environment" - type = string -} - -variable "db_engine_version" { - description = "MySQL engine version for the stage environment" - type = string -} - -variable "db_parameter_group_name" { - description = "MySQL parameter group name for the stage environment" - type = string -} - -variable "db_root_username" { - description = "DB Username for stage" - type = string -} - -variable "db_root_password" { - description = "DB Password for stage" - type = string - sensitive = true -} - -variable "additional_db_users" { - description = "추가 DB 유저 및 권한 목록" - type = map(object({ - password = string - database = string - privileges = list(string) - })) -} - variable "key_name" { description = "Key pair name" type = string } -variable "kms_key_arn" { - description = "Existing KMS Key ARN for stage DB Encryption" - type = string -} - variable "domain_name" { description = "Domain name for the stage environment" type = string diff --git a/modules/app_stack/ec2.tf b/modules/app_stack/ec2.tf index 9b0d1c7..b49aa52 100644 --- a/modules/app_stack/ec2.tf +++ b/modules/app_stack/ec2.tf @@ -32,6 +32,7 @@ resource "aws_instance" "api_server" { key_name = var.key_name associate_public_ip_address = true + iam_instance_profile = var.ec2_iam_instance_profile user_data_base64 = data.cloudinit_config.app_init.rendered diff --git a/modules/app_stack/provider.tf b/modules/app_stack/provider.tf index b1a1d17..c756fdc 100644 --- a/modules/app_stack/provider.tf +++ b/modules/app_stack/provider.tf @@ -14,10 +14,3 @@ terraform { } } } - -# MySQL Provider 설정 (SSH 터널링을 통해 로컬호스트로 접속 가정) -provider "mysql" { - endpoint = "127.0.0.1:3306" - username = var.db_username - password = var.db_password -} diff --git a/modules/app_stack/rds.tf b/modules/app_stack/rds.tf index 8f484c1..47d4f0d 100644 --- a/modules/app_stack/rds.tf +++ b/modules/app_stack/rds.tf @@ -1,5 +1,7 @@ # 5. RDS resource "aws_db_instance" "default" { + count = var.enable_rds ? 1 : 0 + identifier = var.rds_identifier allocated_storage = 20 engine = "mysql" @@ -10,7 +12,7 @@ resource "aws_db_instance" "default" { parameter_group_name = var.db_parameter_group_name copy_tags_to_snapshot = true skip_final_snapshot = true - vpc_security_group_ids = [aws_security_group.db_sg.id] + vpc_security_group_ids = [aws_security_group.db_sg[count.index].id] storage_encrypted = true kms_key_id = var.kms_key_arn @@ -22,7 +24,7 @@ resource "aws_db_instance" "default" { # 6. MySQL 추가 유저 생성 resource "mysql_user" "users" { - for_each = var.additional_db_users + for_each = var.enable_rds ? var.additional_db_users : {} user = each.key host = "%" @@ -33,7 +35,7 @@ resource "mysql_user" "users" { # 7. MySQL 권한 부여 resource "mysql_grant" "user_grants" { - for_each = var.additional_db_users + for_each = var.enable_rds ? var.additional_db_users : {} user = each.key host = "%" diff --git a/modules/app_stack/security_groups.tf b/modules/app_stack/security_groups.tf index 5ec8b31..1a10fbd 100644 --- a/modules/app_stack/security_groups.tf +++ b/modules/app_stack/security_groups.tf @@ -1,15 +1,3 @@ -data "aws_instance" "monitoring_ec2" { - filter { - name = "tag:Name" - values = ["solid-connection-monitoring"] - } - - filter { - name = "instance-state-name" - values = ["running"] - } -} - # 1. API Server용 보안 그룹 (SSH 연결 허용) resource "aws_security_group" "api_sg" { name = "sc-${var.env_name}-api-sg" @@ -27,15 +15,6 @@ resource "aws_security_group" "api_sg" { } } - ingress { - description = "Allow 8081 from EC2: (${data.aws_instance.monitoring_ec2.tags.Name})" - from_port = 8081 - to_port = 8081 - protocol = "tcp" - - cidr_blocks = ["${data.aws_instance.monitoring_ec2.private_ip}/32"] - } - # [Outbound] 모든 트래픽 허용 egress { from_port = 0 @@ -51,6 +30,7 @@ resource "aws_security_group" "api_sg" { # 2. RDS용 보안 그룹 (API Server만 믿음) resource "aws_security_group" "db_sg" { + count = var.enable_rds ? 1 : 0 name = "sc-${var.env_name}-db-sg" description = "Security Group for RDS" vpc_id = var.vpc_id diff --git a/modules/app_stack/variables.tf b/modules/app_stack/variables.tf index 68d1013..33f8b1a 100644 --- a/modules/app_stack/variables.tf +++ b/modules/app_stack/variables.tf @@ -6,8 +6,21 @@ variable "instance_type" { description = "EC2 인스턴스 타입" } +variable "enable_rds" { + description = "RDS 사용 여부" + type = bool + default = true +} + +variable "ec2_iam_instance_profile" { + description = "EC2에 연결할 IAM Instance Profile 이름" + type = string + default = null +} + variable "db_instance_class" { description = "RDS 인스턴스 타입" + default = null } variable "api_ingress_rules" { @@ -29,18 +42,21 @@ variable "db_ingress_rules" { protocol = string description = string })) + default = [] } # [DB 관련 추가 변수] variable "db_username" { description = "DB 마스터 사용자명" type = string + default = "" } variable "db_password" { description = "DB 마스터 비밀번호" type = string sensitive = true + default = "" } # 추가할 DB 유저 목록 @@ -57,21 +73,25 @@ variable "additional_db_users" { variable "db_engine_version" { description = "MySQL 엔진 버전" type = string + default = null } variable "db_parameter_group_name" { description = "MySQL 엔진 파라미터 그룹" type = string + default = null } variable "rds_identifier" { description = "RDS DB Identifier" type = string + default = null } variable "kms_key_arn" { description = "RDS 스토리지 암호화를 위한 KMS Key ARN" type = string + default = null } variable "vpc_id" { diff --git a/modules/shared_resources/acm.tf b/modules/shared_resources/acm.tf index 9dc9b66..af78dfd 100644 --- a/modules/shared_resources/acm.tf +++ b/modules/shared_resources/acm.tf @@ -1,17 +1,3 @@ -resource "aws_acm_certificate" "default_cdn_cert" { - provider = aws.virginia - domain_name = "cdn.default.solid-connection.com" - validation_method = "DNS" - - tags = { - Name = "cdn-default-solid-connection-cert" - } - - lifecycle { - create_before_destroy = true - } -} - resource "aws_acm_certificate" "upload_cdn_cert" { provider = aws.virginia domain_name = "cdn.upload.solid-connection.com" diff --git a/modules/shared_resources/cloudfront.tf b/modules/shared_resources/cloudfront.tf index a6a75a0..0fb1d95 100644 --- a/modules/shared_resources/cloudfront.tf +++ b/modules/shared_resources/cloudfront.tf @@ -1,22 +1,9 @@ # 0. S3 bucket Information read (Data Source) -data "aws_s3_bucket" "default" { - bucket = var.s3_default_bucket_name -} - data "aws_s3_bucket" "upload" { bucket = var.s3_upload_bucket_name } # 1. OAC (Origin Access Control) 리소스 정의 -# 하드코딩된 ID 대신, 테라폼 리소스로 관리하여 ID를 동적으로 참조합니다. -resource "aws_cloudfront_origin_access_control" "default_oac" { - name = "default-oac-${var.s3_default_bucket_name}" - description = "OAC for Default Bucket" - origin_access_control_origin_type = "s3" - signing_behavior = "always" - signing_protocol = "sigv4" -} - resource "aws_cloudfront_origin_access_control" "upload_oac" { name = "upload-oac-${var.s3_upload_bucket_name}" description = "OAC for Upload Bucket" @@ -25,60 +12,7 @@ resource "aws_cloudfront_origin_access_control" "upload_oac" { signing_protocol = "sigv4" } -# 2. CDN for Default Bucket -resource "aws_cloudfront_distribution" "default_cdn" { - enabled = true - is_ipv6_enabled = true - comment = "solid-connection s3 default cloudfront" - price_class = "PriceClass_All" - http_version = "http2" - - web_acl_id = var.default_cdn_web_acl_id - - tags = { - "Name" = "solid-connection s3 default cloudfront" - } - - aliases = [aws_acm_certificate.default_cdn_cert.domain_name] - - origin { - domain_name = data.aws_s3_bucket.default.bucket_regional_domain_name - origin_id = "S3-${var.s3_default_bucket_name}" - origin_access_control_id = aws_cloudfront_origin_access_control.default_oac.id - - connection_attempts = 3 - connection_timeout = 10 - } - - default_cache_behavior { - target_origin_id = "S3-${var.s3_default_bucket_name}" # 위 origin_id와 같아야 함 - viewer_protocol_policy = "redirect-to-https" - compress = true - - allowed_methods = ["GET", "HEAD"] - cached_methods = ["GET", "HEAD"] - - cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" - - smooth_streaming = false - } - - restrictions { - geo_restriction { - restriction_type = "none" - locations = [] - } - } - - viewer_certificate { - cloudfront_default_certificate = false - acm_certificate_arn = aws_acm_certificate.default_cdn_cert.arn - minimum_protocol_version = "TLSv1.2_2021" - ssl_support_method = "sni-only" - } -} - -# 3. CDN for Upload Bucket +# 2. CDN for Upload Bucket resource "aws_cloudfront_distribution" "upload_cdn" { enabled = true is_ipv6_enabled = true diff --git a/modules/shared_resources/lambda.tf b/modules/shared_resources/lambda.tf index 2865d2f..e6248e3 100644 --- a/modules/shared_resources/lambda.tf +++ b/modules/shared_resources/lambda.tf @@ -44,7 +44,7 @@ resource "aws_lambda_permission" "allow_s3_resizing" { action = "lambda:InvokeFunction" function_name = aws_lambda_function.resizing_img_func.function_name principal = "s3.amazonaws.com" - source_arn = aws_s3_bucket.default.arn + source_arn = aws_s3_bucket.upload.arn } resource "aws_lambda_permission" "allow_s3_thumbnail" { @@ -52,12 +52,12 @@ resource "aws_lambda_permission" "allow_s3_thumbnail" { action = "lambda:InvokeFunction" function_name = aws_lambda_function.thumbnail_generating_func.function_name principal = "s3.amazonaws.com" - source_arn = aws_s3_bucket.default.arn + source_arn = aws_s3_bucket.upload.arn } # 4. S3 Trigger Setting resource "aws_s3_bucket_notification" "bucket_notification" { - bucket = aws_s3_bucket.default.id + bucket = aws_s3_bucket.upload.id lambda_function { lambda_function_arn = aws_lambda_function.resizing_img_func.arn diff --git a/modules/shared_resources/output.tf b/modules/shared_resources/output.tf index 44c0c10..b771123 100644 --- a/modules/shared_resources/output.tf +++ b/modules/shared_resources/output.tf @@ -1,12 +1,6 @@ output "acm_validation_records" { description = "Cloudflare에 등록해야 할 인증서 검증용 CNAME 값들 (Proxy Off 필수!)" value = { - default_cdn = { - for dvo in aws_acm_certificate.default_cdn_cert.domain_validation_options : dvo.domain_name => { - name = dvo.resource_record_name - record = dvo.resource_record_value - } - } upload_cdn = { for dvo in aws_acm_certificate.upload_cdn_cert.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name diff --git a/modules/shared_resources/s3.tf b/modules/shared_resources/s3.tf index 9a158ab..79b9e89 100644 --- a/modules/shared_resources/s3.tf +++ b/modules/shared_resources/s3.tf @@ -1,15 +1,4 @@ # 8. S3 Buckets -resource "aws_s3_bucket" "default" { - bucket = var.s3_default_bucket_name - - force_destroy = false - - lifecycle { - prevent_destroy = true - ignore_changes = [tags_all] - } -} - resource "aws_s3_bucket" "upload" { bucket = var.s3_upload_bucket_name diff --git a/modules/shared_resources/variables.tf b/modules/shared_resources/variables.tf index 7d78475..8cb35ab 100644 --- a/modules/shared_resources/variables.tf +++ b/modules/shared_resources/variables.tf @@ -1,9 +1,4 @@ # [S3 버킷 관련 변수] -variable "s3_default_bucket_name" { - description = "Name of the default S3 bucket" - type = string -} - variable "s3_upload_bucket_name" { description = "Name of the upload S3 bucket" type = string @@ -60,11 +55,6 @@ variable "thumbnail_generating_func_layers" { type = list(string) } -variable "default_cdn_web_acl_id" { - description = "WAF Web ACL Id for Default Cloudfront CDN" - type = string -} - variable "upload_cdn_web_acl_id" { description = "WAF Web ACL Id for Upload Cloudfront CDN" type = string