From 9f269d36a9e31705209df128176f030e56a86aa5 Mon Sep 17 00:00:00 2001 From: devmizz Date: Fri, 14 Feb 2025 22:45:03 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20CI/CD=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yappu-world-server-lambda-cd.yaml | 72 +++++++++ .../yappu-world-server-lambda-ci.yaml | 53 ++++++ .gitignore | 1 + build.gradle.kts | 18 ++- settings.gradle.kts | 2 +- src/main/kotlin/discord/DiscordClient.kt | 4 +- .../kotlin/discord/DiscordWebhookResponse.kt | 4 +- .../handler/SentryDiscordWebhookHandler.kt | 8 +- .../SentryDiscordWebhookHandlerTest.kt | 27 ++++ .../SentryDiscordWebhookHandlerFixture.kt | 153 ++++++++++++++++++ 10 files changed, 329 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/yappu-world-server-lambda-cd.yaml create mode 100644 .github/workflows/yappu-world-server-lambda-ci.yaml create mode 100644 src/test/kotlin/handler/SentryDiscordWebhookHandlerTest.kt create mode 100644 src/test/kotlin/support/fixture/SentryDiscordWebhookHandlerFixture.kt diff --git a/.github/workflows/yappu-world-server-lambda-cd.yaml b/.github/workflows/yappu-world-server-lambda-cd.yaml new file mode 100644 index 0000000..2569954 --- /dev/null +++ b/.github/workflows/yappu-world-server-lambda-cd.yaml @@ -0,0 +1,72 @@ +name: yappu-world-server-lambda-cd + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + cache: gradle + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Create .env file + run: | + mkdir -p src/main/resources + echo "DISCORD_SERVER_ALERT_WEBHOOK=${{ secrets.DISCORD_SERVER_ALERT_WEBHOOK }}" > src/main/resources/.env + cat src/main/resources/.env + + - name: Build ShadowJar + run: | + ./gradlew shadowJar + + # Github Action 실행 서버 IP 추출 + - name: Get Github Actions IP + id: ip + uses: candidob/get-runner-ip@v1.0.0 + + # AWS Credentials + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_BOT_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_BOT_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Deploy to AWS Lambda + run: | + aws lambda update-function-code \ + --function-name sendSentryAlertsToDiscord \ + --zip-file fileb://build/libs/yappu-world-server-lambda-shadow.jar + + # Discord Notification + - name: CD Success Notification + uses: sarisia/actions-status-discord@v1 + if: success() + with: + title: ✅ Lambda 배포 성공 ✅ + webhook: ${{ secrets.DISCORD_SERVER_WEBHOOK }} + color: 0x00FF00 + username: 페페훅 + + - name: CD Failure Notification + uses: sarisia/actions-status-discord@v1 + if: failure() + with: + title: ❗️Lambda 배포 실패 ❗️ + webhook: ${{ secrets.DISCORD_SERVER_WEBHOOK }} + color: 0xFF0000 + username: 페페훅 diff --git a/.github/workflows/yappu-world-server-lambda-ci.yaml b/.github/workflows/yappu-world-server-lambda-ci.yaml new file mode 100644 index 0000000..c95c8a9 --- /dev/null +++ b/.github/workflows/yappu-world-server-lambda-ci.yaml @@ -0,0 +1,53 @@ +name: yappu-world-server-lambda-ci + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + cache: gradle + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Create .env file + run: | + mkdir -p src/main/resources + echo "DISCORD_SERVER_ALERT_WEBHOOK=${{ secrets.DISCORD_SERVER_TEST_WEBHOOK }}" > src/main/resources/.env + cat src/main/resources/.env + + - name: Compile and run test + run: | + ./gradlew check + + # Discord Notification + - name: CI Success Notification + uses: sarisia/actions-status-discord@v1 + if: success() + with: + title: ✅ Lambda CI 성공 ✅ + webhook: ${{ secrets.DISCORD_SERVER_WEBHOOK }} + color: 0x00FF00 + username: 페페훅 + + - name: CI Failure Notification + uses: sarisia/actions-status-discord@v1 + if: failure() + with: + title: ❗️Lambda CI 실패 ❗️ + webhook: ${{ secrets.DISCORD_SERVER_WEBHOOK }} + color: 0xFF0000 + username: 페페훅 diff --git a/.gitignore b/.gitignore index c52dc27..210f912 100644 --- a/.gitignore +++ b/.gitignore @@ -359,3 +359,4 @@ gradle-app.setting # End of https://www.toptal.com/developers/gitignore/api/intellij,intellij+iml,intellij+all,kotlin,gradle,linux,macos,windows src/main/resources/.env +.secrets diff --git a/build.gradle.kts b/build.gradle.kts index 12d73cc..b995211 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,11 +21,21 @@ dependencies { implementation("io.github.cdimascio:dotenv-kotlin:6.5.0") testImplementation(kotlin("test")) + testImplementation("io.mockk:mockk:1.13.14") } -tasks.test { - useJUnitPlatform() -} kotlin { jvmToolchain(17) -} \ No newline at end of file +} + +tasks { + test { + useJUnitPlatform() + } + + shadowJar { + archiveBaseName.set("yappu-world-server-lambda") // JAR 기본 이름 + archiveVersion.set("") // 버전 제거 + archiveClassifier.set("shadow") // 기본 bootJar와 구분 + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 5116e0d..74fa692 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } -rootProject.name = "yappu-world-sentry-lambda" +rootProject.name = "yappu-world-server-lambda" diff --git a/src/main/kotlin/discord/DiscordClient.kt b/src/main/kotlin/discord/DiscordClient.kt index b7eaf1c..8f5fc3e 100644 --- a/src/main/kotlin/discord/DiscordClient.kt +++ b/src/main/kotlin/discord/DiscordClient.kt @@ -12,14 +12,14 @@ class DiscordClient { suspend fun send(message: DiscordMessage): DiscordWebhookResponse { val dotenv = Dotenv.load() val response = HttpClient(CIO).use { client -> - client.post(dotenv["DISCORD_WEBHOOK"]) { + client.post(dotenv["DISCORD_SERVER_ALERT_WEBHOOK"]) { contentType(ContentType.Application.Json) setBody(jacksonObjectMapper().writeValueAsString(message)) } } return DiscordWebhookResponse( - response.status, + response.status.value, response.body() ) } diff --git a/src/main/kotlin/discord/DiscordWebhookResponse.kt b/src/main/kotlin/discord/DiscordWebhookResponse.kt index 1488aba..e84ca87 100644 --- a/src/main/kotlin/discord/DiscordWebhookResponse.kt +++ b/src/main/kotlin/discord/DiscordWebhookResponse.kt @@ -1,8 +1,6 @@ package co.discord -import io.ktor.http.* - data class DiscordWebhookResponse( - val status: HttpStatusCode, + val status: Int, val response: String ) diff --git a/src/main/kotlin/handler/SentryDiscordWebhookHandler.kt b/src/main/kotlin/handler/SentryDiscordWebhookHandler.kt index 4840bc1..19f53e4 100644 --- a/src/main/kotlin/handler/SentryDiscordWebhookHandler.kt +++ b/src/main/kotlin/handler/SentryDiscordWebhookHandler.kt @@ -10,7 +10,9 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import kotlinx.coroutines.runBlocking -class SentryDiscordWebhookHandler : RequestHandler { +class SentryDiscordWebhookHandler( + private val discordClient: DiscordClient = DiscordClient() +) : RequestHandler { override fun handleRequest(input: Any, context: Context): APIGatewayProxyResponseEvent { val event = parseEvent(input) @@ -31,7 +33,7 @@ class SentryDiscordWebhookHandler : RequestHandler } -} \ No newline at end of file +} diff --git a/src/test/kotlin/handler/SentryDiscordWebhookHandlerTest.kt b/src/test/kotlin/handler/SentryDiscordWebhookHandlerTest.kt new file mode 100644 index 0000000..f8ecacb --- /dev/null +++ b/src/test/kotlin/handler/SentryDiscordWebhookHandlerTest.kt @@ -0,0 +1,27 @@ +package handler + +import co.discord.DiscordClient +import co.handler.SentryDiscordWebhookHandler +import com.amazonaws.services.lambda.runtime.Context +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.mockk.mockk +import org.junit.jupiter.api.Test +import support.fixture.SentryDiscordWebhookHandlerFixture +import kotlin.test.assertTrue + +class SentryDiscordWebhookHandlerTest { + + val mockContext = mockk() + + // 실행 결과는 test-webhook channel을 확인하세요 + @Test + fun `데이터를 파싱하여 디스코드에 메세지를 보낸다`() { + val responseEvent = SentryDiscordWebhookHandler(DiscordClient()).handleRequest( + SentryDiscordWebhookHandlerFixture.body, + mockContext + ) + + val parsedResponseBody = jacksonObjectMapper().readValue(responseEvent.body, Map::class.java) + assertTrue { (parsedResponseBody["status"].toString()).startsWith("2") } + } +} \ No newline at end of file diff --git a/src/test/kotlin/support/fixture/SentryDiscordWebhookHandlerFixture.kt b/src/test/kotlin/support/fixture/SentryDiscordWebhookHandlerFixture.kt new file mode 100644 index 0000000..9630871 --- /dev/null +++ b/src/test/kotlin/support/fixture/SentryDiscordWebhookHandlerFixture.kt @@ -0,0 +1,153 @@ +package support.fixture + +object SentryDiscordWebhookHandlerFixture { + val body = mapOf( + "action" to "triggered", + "actor" to mapOf( + "id" to "sentry", + "name" to "Sentry", + "type" to "application" + ), + "data" to mapOf( + "event" to mapOf( + "_ref" to 1, + "_ref_version" to 2, + "contexts" to mapOf( + "browser" to mapOf( + "name" to "Chrome", + "type" to "browser", + "version" to "75.0.3770" + ), + "os" to mapOf( + "name" to "Mac OS X", + "type" to "os", + "version" to "10.14.0" + ) + ), + "culprit" to "?()", + "datetime" to "2019-08-19T21:06:17.677000Z", + "dist" to null, + "event_id" to "e4874d664c3540c1a32eab185f12c5ab", + "exception" to mapOf( + "values" to listOf( + mapOf( + "mechanism" to mapOf( + "data" to mapOf( + "message" to "heck is not defined", + "mode" to "stack", + "name" to "ReferenceError" + ), + "description" to null, + "handled" to false, + "help_link" to null, + "meta" to null, + "synthetic" to null, + "type" to "onerror" + ), + "stacktrace" to mapOf( + "frames" to listOf( + mapOf( + "abs_path" to "https://static.jsbin.com/js/prod/runner-4.1.7.min.js", + "colno" to 10866, + "context_line" to "{snip} e(a.old),a.active=b,e(a.target,b),setTimeout(function(){c&&c();for(var b,d=a.target.getElementsByTagName(\"iframe\"),e=d.length,f=0,g=a.active {snip}", + "filename" to "/js/prod/runner-4.1.7.min.js", + "function" to null, + "image_addr" to null, + "in_app" to false, + "lineno" to 1, + "module" to "prod/runner-4.1.7" + ) + ) + ), + "type" to "ReferenceError", + "value" to "heck is not defined" + ) + ) + ), + "fingerprint" to listOf("{{ default }}"), + "issue_url" to "https://sentry.io/api/0/issues/1117540176/", + "issue_id" to "1117540176", + "level" to "error", + "location" to "", + "message" to "", + "metadata" to mapOf( + "filename" to "", + "type" to "ReferenceError", + "value" to "heck is not defined" + ), + "platform" to "javascript", + "project" to 1, + "received" to 1566248777.677, + "release" to null, + "request" to mapOf( + "cookies" to null, + "data" to null, + "env" to null, + "fragment" to null, + "headers" to listOf( + listOf( + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" + ) + ), + "method" to null, + "query_string" to emptyList(), + "url" to "https://null.jsbin.com/runner" + ), + "sdk" to mapOf( + "integrations" to listOf( + "InboundFilters", + "FunctionToString", + "BrowserApiErrors", + "Breadcrumbs", + "GlobalHandlers", + "LinkedErrors", + "HttpContext" + ), + "name" to "sentry.javascript.browser", + "packages" to listOf( + mapOf( + "name" to "npm:@sentry/browser", + "version" to "5.5.0" + ) + ), + "version" to "5.5.0" + ), + "tags" to listOf( + listOf("browser", "Chrome 75.0.3770"), + listOf("browser.name", "Chrome"), + listOf("handled", "no"), + listOf("level", "error"), + listOf("mechanism", "onerror"), + listOf("os", "Mac OS X 10.14.0"), + listOf("os.name", "Mac OS X"), + listOf("user", "ip:162.217.75.90"), + listOf("url", "https://null.jsbin.com/runner") + ), + "time_spent" to null, + "timestamp" to 1566248777.677, + "title" to "Test: 테스트로 만든 예외입니다. 신경 쓰지 마세요.", + "type" to "error", + "url" to "https://sentry.io/api/0/projects/test-org/front-end/events/e4874d664c3540c1a32eab185f12c5ab/", + "user" to mapOf( + "ip_address" to "162.218.85.90" + ), + "version" to "7", + "web_url" to "https://sentry.io/organizations/test-org/issues/1117540176/events/e4874d664c3540c1a32eab185f12c5ab/" + ) + ), + "triggered_rule" to "Very Important Alert Rule!", + "issue_alert" to mapOf( + "title" to "Very Important Alert Rule!", + "settings" to listOf( + mapOf( + "name" to "channel", + "value" to "#general" + ) + ) + ), + "installation" to mapOf( + "uuid" to "a8e5d37a-696c-4c54-adb5-b3f28d64c7de" + ) + ) +} From 58e9f80cd2a3b517f38c7de2f6a48b313c63292a Mon Sep 17 00:00:00 2001 From: devmizz Date: Sat, 15 Feb 2025 00:04:24 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20AWS=20CLI=20Pager=20less=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/yappu-world-server-lambda-cd.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/yappu-world-server-lambda-cd.yaml b/.github/workflows/yappu-world-server-lambda-cd.yaml index 2569954..172333e 100644 --- a/.github/workflows/yappu-world-server-lambda-cd.yaml +++ b/.github/workflows/yappu-world-server-lambda-cd.yaml @@ -33,10 +33,11 @@ jobs: run: | ./gradlew shadowJar - # Github Action 실행 서버 IP 추출 - - name: Get Github Actions IP - id: ip - uses: candidob/get-runner-ip@v1.0.0 + - name: Install AWS CLI + run: | + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install # AWS Credentials - name: Configure AWS Credentials @@ -48,9 +49,10 @@ jobs: - name: Deploy to AWS Lambda run: | + export AWS_PAGER="" aws lambda update-function-code \ --function-name sendSentryAlertsToDiscord \ - --zip-file fileb://build/libs/yappu-world-server-lambda-shadow.jar + --zip-file fileb://build/libs/yappu-world-server-lambda-shadow.jar # Discord Notification - name: CD Success Notification