Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.github
.gradle
build
logs
.env
*.env
*.log
.idea
171 changes: 171 additions & 0 deletions .github/workflows/deploy-quality-service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
name: Build Quality Service and Update GitOps

on:
push:
branches: ["dev", "main"]
paths:
- "src/**"
- "build.gradle"
- "build.gradle.kts"
- "settings.gradle"
- "settings.gradle.kts"
- "gradlew"
- "gradle/**"
- "Dockerfile"
- ".dockerignore"
- ".github/workflows/deploy-quality-service.yml"
workflow_dispatch:

concurrency:
group: quality-service-${{ github.ref_name }}
cancel-in-progress: true

env:
AWS_REGION: ap-northeast-2
EKS_CLUSTER_NAME: aims-dev-eks
ECR_REPOSITORY: aims/quality-service
NAMESPACE: aims-project
QUALITY_SERVICE_CONTEXT: .
INFRA_REPOSITORY: SK-Rookies-AIMS/infra
INFRA_MANIFEST_PATH: k8s/quality-service/quality-service.yaml

jobs:
deploy:
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Checkout quality-service repository
uses: actions/checkout@v4

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Ensure ECR repository exists
run: |
aws ecr describe-repositories --repository-names "$ECR_REPOSITORY" --region "$AWS_REGION" >/dev/null 2>&1 || aws ecr create-repository --repository-name "$ECR_REPOSITORY" --region "$AWS_REGION" --image-scanning-configuration scanOnPush=true --image-tag-mutability MUTABLE --encryption-configuration encryptionType=AES256

- name: Build and push Docker image
id: build-image
run: |
IMAGE_TAG="${GITHUB_REF_NAME}-${GITHUB_SHA::12}"
IMAGE_URI="${{ steps.login-ecr.outputs.registry }}/${ECR_REPOSITORY}:${IMAGE_TAG}"
BRANCH_IMAGE_URI="${{ steps.login-ecr.outputs.registry }}/${ECR_REPOSITORY}:${GITHUB_REF_NAME}"

docker build -t "$IMAGE_URI" -t "$BRANCH_IMAGE_URI" "$QUALITY_SERVICE_CONTEXT"
docker push "$IMAGE_URI"
docker push "$BRANCH_IMAGE_URI"

echo "image_uri=$IMAGE_URI" >> "$GITHUB_OUTPUT"

- name: Checkout infra repository
uses: actions/checkout@v4
with:
repository: ${{ env.INFRA_REPOSITORY }}
ref: ${{ github.ref_name }}
token: ${{ secrets.INFRA_REPO_TOKEN }}
path: infra
fetch-depth: 0

- name: Update kubeconfig and ensure namespace
if: github.ref_name == 'dev'
run: |
aws eks update-kubeconfig --region "$AWS_REGION" --name "$EKS_CLUSTER_NAME"
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -

- name: Load quality-service runtime parameters from SSM
if: github.ref_name == 'dev'
run: |
RDS_SECRET_ARN=$(aws ssm get-parameter --name "/aims/dev/rds/secret-arn" --region "$AWS_REGION" --query "Parameter.Value" --output text)
RDS_HOST=$(aws ssm get-parameter --name "/aims/dev/backend/rds-host" --region "$AWS_REGION" --query "Parameter.Value" --output text)
RDS_PORT=$(aws ssm get-parameter --name "/aims/dev/backend/rds-port" --region "$AWS_REGION" --query "Parameter.Value" --output text)
MAIN_DB_NAME=$(aws ssm get-parameter --name "/aims/dev/backend/main-db-name" --region "$AWS_REGION" --query "Parameter.Value" --output text)
SAMPLE_DB_NAME=$(aws ssm get-parameter --name "/aims/dev/backend/sample-db-name" --region "$AWS_REGION" --query "Parameter.Value" --output text)
JWT_SECRET_KEY=$(aws ssm get-parameter --name "/aims/dev/backend/jwt-secret-key" --region "$AWS_REGION" --with-decryption --query "Parameter.Value" --output text)

for VALUE in "$RDS_SECRET_ARN" "$RDS_HOST" "$RDS_PORT" "$MAIN_DB_NAME" "$SAMPLE_DB_NAME" "$JWT_SECRET_KEY"; do
if [ -z "$VALUE" ] || [ "$VALUE" = "None" ]; then
echo "Required quality-service SSM parameter is empty"
exit 1
fi
done

echo "::add-mask::$RDS_SECRET_ARN"
echo "::add-mask::$JWT_SECRET_KEY"

echo "RDS_SECRET_ARN=$RDS_SECRET_ARN" >> "$GITHUB_ENV"
echo "RDS_HOST=$RDS_HOST" >> "$GITHUB_ENV"
echo "RDS_PORT=$RDS_PORT" >> "$GITHUB_ENV"
echo "MAIN_DB_NAME=$MAIN_DB_NAME" >> "$GITHUB_ENV"
echo "SAMPLE_DB_NAME=$SAMPLE_DB_NAME" >> "$GITHUB_ENV"
echo "JWT_SECRET_KEY=$JWT_SECRET_KEY" >> "$GITHUB_ENV"

- name: Create or update quality-service Kubernetes Secret
if: github.ref_name == 'dev'
run: |
SECRET_JSON=$(aws secretsmanager get-secret-value --secret-id "$RDS_SECRET_ARN" --region "$AWS_REGION" --query "SecretString" --output text)

DB_USERNAME=$(echo "$SECRET_JSON" | jq -r '.username')
DB_PASSWORD=$(echo "$SECRET_JSON" | jq -r '.password')

if [ -z "$DB_USERNAME" ] || [ "$DB_USERNAME" = "null" ] || [ -z "$DB_PASSWORD" ] || [ "$DB_PASSWORD" = "null" ]; then
echo "RDS username or password is missing"
exit 1
fi

MAIN_DB_JDBC_URL="jdbc:mysql://$RDS_HOST:$RDS_PORT/$MAIN_DB_NAME?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8"
SAMPLE_DB_JDBC_URL="jdbc:mysql://$RDS_HOST:$RDS_PORT/$SAMPLE_DB_NAME?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8"

echo "::add-mask::$DB_USERNAME"
echo "::add-mask::$DB_PASSWORD"
echo "::add-mask::$MAIN_DB_JDBC_URL"
echo "::add-mask::$SAMPLE_DB_JDBC_URL"

kubectl create secret generic quality-service-runtime-secret -n "$NAMESPACE" --from-literal=MAIN_DB_JDBC_URL="$MAIN_DB_JDBC_URL" --from-literal=MAIN_DB_USERNAME="$DB_USERNAME" --from-literal=MAIN_DB_PASSWORD="$DB_PASSWORD" --from-literal=SAMPLE_DB_JDBC_URL="$SAMPLE_DB_JDBC_URL" --from-literal=SAMPLE_DB_USERNAME="$DB_USERNAME" --from-literal=SAMPLE_DB_PASSWORD="$DB_PASSWORD" --from-literal=JWT_SECRET_KEY="$JWT_SECRET_KEY" --dry-run=client -o yaml | kubectl apply -f -

- name: Update quality-service image in GitOps repository
env:
IMAGE_URI: ${{ steps.build-image.outputs.image_uri }}
run: |
MANIFEST="infra/${INFRA_MANIFEST_PATH}"

test -f "$MANIFEST"
sed -i -E "s|^([[:space:]]*)image:.*$|\1image: ${IMAGE_URI}|" "$MANIFEST"
grep -n "image:" "$MANIFEST"

cd infra

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git add "$INFRA_MANIFEST_PATH"

if git diff --cached --quiet; then
echo "Image tag is already up to date"
exit 0
fi

git commit -m "chore(quality-service): deploy ${GITHUB_REF_NAME}-${GITHUB_SHA::12}"

for ATTEMPT in 1 2 3; do
git pull --rebase origin "$GITHUB_REF_NAME"

if git push origin "HEAD:$GITHUB_REF_NAME"; then
exit 0
fi

sleep $((ATTEMPT * 5))
done

echo "Failed to push GitOps image update"
exit 1
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ out/

### VS Code ###
.vscode/

# Application logs
logs/
*.log
32 changes: 32 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM eclipse-temurin:17-jdk-jammy AS builder

WORKDIR /workspace

COPY gradlew gradlew
COPY gradle gradle
COPY build.gradle settings.gradle ./

RUN sed -i 's/\r$//' gradlew && chmod +x gradlew

COPY src src

RUN ./gradlew clean bootJar -x test --no-daemon

RUN JAR_FILE=$(find build/libs -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1) && test -n "$JAR_FILE" && cp "$JAR_FILE" build/libs/app.jar


FROM eclipse-temurin:17-jre-jammy

WORKDIR /app

RUN groupadd --system --gid 10001 spring && useradd --system --uid 10001 --gid 10001 --home-dir /app --shell /usr/sbin/nologin spring

COPY --from=builder /workspace/build/libs/app.jar /app/app.jar

RUN chown -R 10001:10001 /app

USER 10001:10001

EXPOSE 8083

ENTRYPOINT ["java","-XX:MaxRAMPercentage=75.0","-Djava.security.egd=file:/dev/./urandom","-jar","/app/app.jar"]
56 changes: 28 additions & 28 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.12'
id 'io.spring.dependency-management' version '1.1.7'
id 'java'
id 'org.springframework.boot' version '3.3.12'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.aims'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.kafka:spring-kafka'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.kafka:spring-kafka'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-actuator-test'
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
testImplementation 'org.springframework.boot:spring-boot-starter-data-redis-test'
testImplementation 'org.springframework.kafka:spring-kafka-test'
testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-validation-test'
testImplementation 'org.springframework.boot:spring-boot-starter-web-test'
testCompileOnly 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.kafka:spring-kafka-test'
testImplementation 'org.springframework.security:spring-security-test'

testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
useJUnitPlatform()
}
useJUnitPlatform()
}
3 changes: 2 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

Loading
Loading