Skip to content
Merged

Main #306

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
- 'src/**'
- 'build.gradle'
- 'Dockerfile'
- 'ecs-task-definitions/api-task-definition.json'
- 'api-task-definition.json' # 수정된 부분
- '.github/workflows/deploy-api.yml'
workflow_dispatch:
inputs:
Expand All @@ -21,7 +21,7 @@ env:
AWS_REGION: ap-northeast-2
ECR_REPOSITORY: survey-api/service
ECS_CLUSTER: survey-cluster
ECS_SERVICE: api-service
ECS_SERVICE: api-task-service-v12
ECS_TASK_DEFINITION: api-task-definition.json

jobs:
Expand Down Expand Up @@ -59,10 +59,24 @@ jobs:
- name: Build application
run: ./gradlew bootJar -x test

- name: 'Debug: Dump OIDC Token Payload'
uses: actions/github-script@v6
with:
script: |
try {
const token = await core.getIDToken();
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
console.log('--- OIDC Token Payload ---');
console.log(JSON.stringify(payload, null, 2));
console.log('--------------------------');
} catch (error) {
core.setFailed(`Error dumping OIDC token: ${error.message}`);
}

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::025861172546:role/github-actions-role # 실제 ARN으로 교체 필요
role-to-assume: arn:aws:iam::025861172546:role/github-actions-role
role-session-name: GitHub-Actions-Survey-API
aws-region: ${{ env.AWS_REGION }}

Expand Down Expand Up @@ -97,7 +111,7 @@ jobs:
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ecs-task-definitions/${{ env.ECS_TASK_DEFINITION }}
task-definition: ${{ env.ECS_TASK_DEFINITION }} # 수정된 부분
container-name: api-container
image: ${{ steps.build-image.outputs.image }}

Expand Down
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
FROM docker.elastic.co/elasticsearch/elasticsearch:8.15.0
FROM eclipse-temurin:17-jre-alpine

RUN bin/elasticsearch-plugin install analysis-nori
COPY build/libs/*.jar app.jar

ENTRYPOINT ["java", "-jar", "/app.jar"]
80 changes: 80 additions & 0 deletions api-task-definition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"family": "api-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["EC2"],
"cpu": "1024",
"memory": "1536",
"executionRoleArn": "arn:aws:iam::025861172546:role/ecsTaskExecutionRole",
"containerDefinitions": [{
"name": "api-container",
"image": "025861172546.dkr.ecr.ap-northeast-2.amazonaws.com/survey-api/service:latest",
"memory": 1024,
"portMappings": [{
"containerPort": 8080,
"protocol": "tcp"
}],
"essential": true,
"secrets": [
{ "name": "DB_USERNAME", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-db/credentials:username::" },
{ "name": "DB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-db/credentials:password::" },

{ "name": "SECRET_KEY", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-api/secrets:SECRET_KEY::" },
{ "name": "RABBITMQ_USERNAME", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-api/secrets:RABBITMQ_USERNAME::" },
{ "name": "RABBITMQ_PASSWORD", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-api/secrets:RABBITMQ_PASSWORD::" },
{ "name": "MONGODB_USERNAME", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-api/secrets:MONGODB_USERNAME::" },
{ "name": "MONGODB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-api/secrets:MONGODB_PASSWORD::" },
{ "name": "NAVER_SECRET", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-api/secrets:NAVER_SECRET::" },
{ "name": "GOOGLE_SECRET", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-api/secrets:GOOGLE_SECRET::" },

{ "name": "FIREBASE_PRIVATE_KEY_ID", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-api/secrets:FIREBASE_PRIVATE_KEY_ID::" },
{ "name": "FIREBASE_PRIVATE_KEY", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:025861172546:secret:prod/survey-api/secrets:FIREBASE_PRIVATE_KEY::" }
],
"environment": [
{"name": "SPRING_PROFILES_ACTIVE", "value": "prod"},
{"name": "SPRING_JPA_HIBERNATE_DDL_AUTO", "value": "none"},
{"name": "DB_HOST", "value": "survey-database.c5gckg46ikaa.ap-northeast-2.rds.amazonaws.com"},
{"name": "DB_PORT", "value": "5432"},
{"name": "DB_SCHEME", "value": "survey_db"},
{"name": "REDIS_HOST", "value": "cache.survey-cache.local"},
{"name": "REDIS_PORT", "value": "6379"},
{"name": "RABBITMQ_HOST", "value": "message.survey-message.local"},
{"name": "RABBITMQ_PORT", "value": "5672"},
{"name": "MONGODB_HOST", "value": "document.survey-document.local"},
{"name": "MONGODB_PORT", "value": "27017"},
{"name": "MONGODB_DATABASE", "value": "survey_read_db"},
{"name": "MONGODB_AUTHDB", "value": "admin"},
{"name": "ELASTIC_URIS", "value": "http://elastic.survey-elastic.local:9200"},
{"name": "ELASTICSEARCH_ENABLED", "value": "true"},
{"name": "SPRING_MAIL_ENABLED", "value": "false"},
{"name": "MANAGEMENT_HEALTH_MAIL_ENABLED", "value": "false"},
{"name": "MANAGEMENT_HEALTH_ELASTICSEARCH_ENABLED", "value": "false"},

{"name": "FIREBASE_ENABLED", "value": "true"},
{"name": "FIREBASE_PROJECT_ID", "value": "survey-f5a93"},
{"name": "FIREBASE_CLIENT_EMAIL", "value": "firebase-adminsdk-fbsvc@survey-f5a93.iam.gserviceaccount.com"},
{"name": "FIREBASE_CLIENT_ID", "value": "100191250643521230154"},

{"name": "KAKAO_CLIENT_ID", "value": "095680b1255e001a75547779c2466dc7"},
{"name": "KAKAO_REDIRECT_URL", "value": "https://api.surveylink.site/auth/kakao/login"},
{"name": "NAVER_CLIENT_ID", "value": "fXngEOOPoc9G6_PYaVwk"},
{"name": "NAVER_REDIRECT_URL", "value": "https://api.surveylink.site/auth/naver/login"},
{"name": "GOOGLE_CLIENT_ID", "value": "143003014057-q9baq71l6sohdveir85j124hbo17hhqu.apps.googleusercontent.com"},
{"name": "GOOGLE_REDIRECT_URL", "value": "https://api.surveylink.site/auth/google/login"}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/api-task",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:8080/actuator/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}]
}
162 changes: 142 additions & 20 deletions src/main/java/com/example/surveyapi/global/config/FcmConfig.java
Original file line number Diff line number Diff line change
@@ -1,38 +1,160 @@
package com.example.surveyapi.global.config;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StringUtils;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class FcmConfig {
@Value("${firebase.credentials.path}")
private String firebaseCredentialsPath;

@Bean
public FirebaseApp firebaseApp() throws IOException {
ClassPathResource resource = new ClassPathResource(firebaseCredentialsPath.replace("classpath:", ""));
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(resource.getInputStream()))
.build();

if(FirebaseApp.getApps().isEmpty()) {
return FirebaseApp.initializeApp(options);
}
return FirebaseApp.getInstance();
}

@Bean
public FirebaseMessaging firebaseMessaging(FirebaseApp firebaseApp) {
return FirebaseMessaging.getInstance(firebaseApp);
}

// 기존 파일 경로 방식 (로컬 개발용)
@Value("${firebase.credentials.path:}")
private String firebaseCredentialsPath;

// 환경변수 방식 (프로덕션용)
@Value("${firebase.project-id:survey-f5a93}")
private String projectId;

@Value("${firebase.private-key-id:}")
private String privateKeyId;

@Value("${firebase.private-key:}")
private String privateKey;

@Value("${firebase.client-email:firebase-adminsdk-fbsvc@survey-f5a93.iam.gserviceaccount.com}")
private String clientEmail;

@Value("${firebase.client-id:100191250643521230154}")
private String clientId;

@Value("${firebase.enabled:true}")
private boolean firebaseEnabled;

@PostConstruct
public void init() {
if (!firebaseEnabled) {
log.info("Firebase is disabled by configuration");
return;
}

if (StringUtils.hasText(firebaseCredentialsPath)) {
log.info("Firebase will be initialized using file: {}", firebaseCredentialsPath);
} else if (StringUtils.hasText(privateKey) && StringUtils.hasText(privateKeyId)) {
log.info("Firebase will be initialized using environment variables");
} else {
log.warn("Firebase credentials not found. Firebase features will be disabled.");
}
}

@Bean
public FirebaseApp firebaseApp() throws IOException {
if (!firebaseEnabled) {
log.warn("Firebase is disabled. Skipping FirebaseApp initialization.");
return null;
}

InputStream credentialsStream = getCredentialsStream();

if (credentialsStream == null) {
log.error("Failed to get Firebase credentials. Firebase features will be disabled.");
return null;
}

try {
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(credentialsStream))
.build();

if (FirebaseApp.getApps().isEmpty()) {
FirebaseApp app = FirebaseApp.initializeApp(options);
log.info("FirebaseApp initialized successfully");
return app;
}
return FirebaseApp.getInstance();
} finally {
credentialsStream.close();
}
}

@Bean
public FirebaseMessaging firebaseMessaging(FirebaseApp firebaseApp) {
if (firebaseApp == null) {
log.warn("FirebaseApp is null. FirebaseMessaging will not be available.");
return null;
}
return FirebaseMessaging.getInstance(firebaseApp);
}

private InputStream getCredentialsStream() throws IOException {
// 1. 먼저 파일 경로 방식 시도 (기존 방식 - 로컬 개발용)
if (StringUtils.hasText(firebaseCredentialsPath)) {
try {
if (firebaseCredentialsPath.startsWith("classpath:")) {
ClassPathResource resource = new ClassPathResource(
firebaseCredentialsPath.replace("classpath:", "")
);
return resource.getInputStream();
} else {
return new FileInputStream(firebaseCredentialsPath);
}
} catch (IOException e) {
log.warn("Failed to load Firebase credentials from file: {}", e.getMessage());
}
}

// 2. 환경변수 방식 시도 (프로덕션용)
if (StringUtils.hasText(privateKey) && StringUtils.hasText(privateKeyId)) {
String firebaseConfig = buildFirebaseConfig();
return new ByteArrayInputStream(firebaseConfig.getBytes(StandardCharsets.UTF_8));
}

// 3. 둘 다 실패한 경우
log.error("No Firebase credentials found. Check firebase.credentials.path or firebase.private-key/firebase.private-key-id");
return null;
}

private String buildFirebaseConfig() {
// 환경변수의 \n을 실제 개행 문자로 변환
String formattedPrivateKey = privateKey.replace("\\n", "\n");

return String.format("""
{
"type": "service_account",
"project_id": "%s",
"private_key_id": "%s",
"private_key": "%s",
"client_email": "%s",
"client_id": "%s",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/%s",
"universe_domain": "googleapis.com"
}
""",
projectId,
privateKeyId,
formattedPrivateKey,
clientEmail,
clientId,
clientEmail.replace("@", "%40")
);
}
}
Loading
Loading