SonarQube + Artifactory ์˜ ์™„๋ฒฝํ•œ ์ž๋™ํ™” ํŒŒ์ดํ”„๋ผ์ธ

์ด ๊ธ€์€ Fabrice Bellingard๊ฐ€ ์ž‘์„ฑํ•œ Fully Automated Promotion Pipelines with SonarQube and Artifactory๋ฅผ ๋ฒˆ์—ญํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค.

์ด ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ๋Š” Jonathan Roquelaure(JFrog)๊ณผ Fabrice Bellingard(SonarSource)๊ฐ€ ๊ณต๋™ ์ž‘์„ฑํ–ˆ์œผ๋ฉฐ, JFrog blog์— ์ตœ์ดˆ๋กœ ๊ฒŒ์žฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

JFrog ์›น์‚ฌ์ดํŠธ์— ๊ฒŒ์ œํ•œ ์ด์ „ ํฌ์ŠคํŠธ๋ฅผ ํ†ตํ•ด Artifactory์™€ SonarQube๋ฅผ ์—ฐ๋™ํ•˜๊ณ , ๋” ๋†’์€ ํ’ˆ์งˆ์˜ ์†Œํ”„ํŠธ์›จ์–ด์— ๋Œ€ํ•œ ๊ฒฐ์ •์„ ๋‚ด๋ฆฌ๋Š” ๋ฐ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Œ์„ ์„ค๋ช…ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด ํŒŒ์ดํ”„๋ผ์ธ์— ๊ฐ„๋‹จํ•œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์†์‰ฝ๊ฒŒ Artifactory์— SonarQube๋ฅผ ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ๊ณ , quality gate ๋งŒ์กฑ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ์ดํ›„์˜ ํŒŒ์ดํ”„๋ผ์ธ ์ง„ํ–‰ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๊ฒŒ ์ „๋ถ€์ผ๊นŒ์š”?

์šฐ๋ฆฌ๋Š” ์•„ํ‹ฐํŒฉํŠธ๋“ค์— quality gate๋ฅผ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์กฐ๊ธˆ ๋” ์ƒ๊ฐํ•ด๋ด…์‹œ๋‹ค. ๋‹จ์ˆœํžˆ ์ด๋ฅผ Artifactory์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ •๋ณด๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹ , ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์ดˆ๋กœ ์ž๋™์ ์œผ๋กœ ํ”„๋กœ๋ชจ์…˜์„ ํŠธ๋ฆฌ๊ฑฐ๋งํ•  ์ˆ˜ (ํ˜น์€ ํ•˜์ง€ ์•Š์„ ์ˆ˜) ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค โ€“ ์˜ˆ๋ฅผ ๋“ค๋ฉด, ๋‹ค์Œ ์Šคํ…Œ์ด์ง€ ํŽ˜์ด์ฆˆ์—์„œ ์•„ํ‹ฐํŒฉํŠธ๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด๋‹น ์•„ํ‹ฐํŒฉํŠธ๋“ค์„ ๋ณต์‚ฌํ•˜๊ฑฐ๋‚˜ ์ด๋™์‹œ์ผœ ๋‘๋Š” ์ž‘์—…๋“ค์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ํšŒ์‚ฌ๋“ค์€ ๊ฐ๊ฐ ๊ณ ์œ ์˜ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ์ž๋™ํ™”๋œ ํ”„๋กœ๋ชจ์…˜์„ ์œ„ํ•ด ํ•„์š”ํ•œ ๋กœ์ง๋“ค์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์•ผ๋งŒ ํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋“ค์€ ๋ณต์žกํ•œ ๋นŒ๋“œ ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์œผ๋ฉฐ, ๊ฐœ๋ฐœํŒ€๋“ค์€ ๊ฐ€๋Šฅํ•œ ๋น ๋ฅธ(Jez Humble๊ณผ Dave Farley๊ฐ€ Continuous Delivery ์ฑ…์—์„œ ๋งํ•œ ๊ฒƒ๊ณผ ๊ฐ™์€ ์ปค๋ฐ‹๋‹น ๋นŒ๋“œ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ํŒŒ์ดํ”„๋ฆฌ์•ˆ๊ณผ ๊ฐ™์€) ํ”ผ๋“œ๋ฐฑ์„ ์›ํ•ฉ๋‹ˆ๋‹ค. SonarQube๋Š” ํ”„๋กœ์ ํŠธ๋ฅผ ๋ถ„์„ํ•ด quality gateย ์ƒํƒœ๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•œ ์•ฝ๊ฐ„์˜ ์‹œ๊ฐ„์ด ํ•„์š”ํ•œ๋ฐ Artifactory์™€ ํ†ตํ•ฉ์„ ํ•˜๋Š” ๊ณผ์ •์—์„œ ํŒŒ์ดํ”„๋ผ์ธ์„ ๋ธ”๋Ÿญ์‹œํ‚ค๋ฉด ์•ˆ๋ฉ๋‹ˆ๋‹ค: ๋˜ํ•œ ์ž ์žฌ์ ์ธ ํ›„์† ์ž‘์—…๋“ค ์—ญ์‹œ SonarQube์˜ ๋ถ„์„ ๋ณด๊ณ ์„œ ์ฒ˜๋ฆฌ์™€ ๊ด€๋ จ์—†์ด ๋™์‹œ์— ์ˆ˜ํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ, Artifactory์™€ SonarQube๋ฅผ ํ™œ์šฉํ•ด ์ด์ œ ํŒŒ์ดํ”„๋ผ์ธ์„ ๋ธ”๋กœํ‚นํ•˜์ง€ ์•Š๊ณ , ์ปค์Šคํ„ฐ๋งˆ์ด์ด์ œ์ด์…˜ ๊ฐ€๋Šฅํ•œ ์ž๋™ํ™” ๋œ ์†”๋ฃจ์…˜์„ ๋งŒ๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ์—ฌ๋Ÿฌ๋ถ„์ด ์ตœ๊ณ  ํ’ˆ์งˆ์˜ ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ง์ž…๋‹ˆ๋‹ค.

์ปค์Šคํ„ฐ๋งˆ์ด์ œ์ด์…˜ ๊ฐ€๋Šฅํ•œ ์ž๋™ํ™” ๋œ ํ†ตํ•ฉ

์—ฌ๋Ÿฌ๋ถ„์€ ์ด ๋‹จ์–ด๋“ค์„ ๋“ฃ๋Š” ์ˆœ๊ฐ„ ์›นํ›…(webhooks), API ๋ฐ ์‚ฌ์šฉ์ž ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์„ ๋– ์˜ฌ๋ฆด ๊ฒƒ์ž…๋‹ˆ๋‹ค.

SoanrQube Webhooks

CI/CD ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๋งŽ์€ ํŒŒ์ดํ”„๋ผ์ธ ์ค‘ ํ•˜๋‚˜๋กœ, SonarQube๋Š” ์›นํ›…(webhooks)์„ ์‚ฌ์šฉํ•ด ๋‹ค๋ฅธ ์„œ๋น„์Šค๋“ค์„ ๋Œ€์ƒ์œผ๋กœ ๋ถ„์„ ๋ณด๊ณ ์„œ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Œ์„ ์•Œ๋ฆฝ๋‹ˆ๋‹ค. HTTPS ์ฝœ์€ ์ฒ˜๋ฆฌ ํƒœ์Šคํฌ์˜ ์ƒํƒœ์— ๊ด€๊ณ„์—†์ด ์ƒ์„ฑ๋˜๋ฉฐ, Artifactory์˜ ์‚ฌ์šฉ์ž ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์ด ์ฝœ์˜ ํŽ˜์ด๋กœ๋“œ์— ๋‹ด๊ธด ๋งŽ์€ ์ •๋ณด๋“ค์„ ํ™œ์šฉํ•ด ์ง€์ •๋œ ์•„ํ‹ฐํŒฉํŠธ๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” SonarQube ์›นํ›…์„ ํ†ตํ•ด ์ „๋‹ฌ๋˜๋Š” JSON ํŽ˜์ด๋กœ๋“œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค:

{ "analysedAt": "2016-11-18T10:46:28+0100", "project": { "key": "org.sonarqube:example", "name": "Example" }, "properties": { }, "qualityGate": { "conditions": [ { "errorThreshold": "1", "metric": "new_security_rating", "onLeakPeriod": true, "operator": "GREATER_THAN", "status": "OK", "value": "1" }, { "errorThreshold": "1", "metric": "new_reliability_rating", "onLeakPeriod": true, "operator": "GREATER_THAN", "status": "ERROR", "value": "1" }, ... ], "name": "SonarQube way", "status": "ERROR" }, "serverUrl": "http://localhost:9000", "status": "SUCCESS", "taskId": "AVh21JS2JepAEhwQ-b3u" }

์ง€๊ธˆ ์šฐ๋ฆฌ๊ฐ€ ์ด์•ผ๊ธฐํ•˜๊ณ  ์žˆ๋Š” ์ปจํ…์ŠคํŠธ์— ํ•ด๋‹นํ•˜๋Š” ์ •๋ณด๋“ค์„ ์ถ”๋ ค๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • TaskId โ€“ Artifactory์˜ ์ง€์ •๋œ ์•„ํ‹ฐํŒฉํŠธ๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค

  • quality gate status โ€“ ์ง€์ •๋œ ์•„ํ‹ฐํŒฉํŠธ์˜ ํ”„๋กœ๋ชจ์…˜ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์ •๋ณด์ž…๋‹ˆ๋‹ค.

SonarQub์˜ ์›นํ›…์€ ํ”„๋กœ์ ํŠธ ๋‹จ์œ„(project settings ๋ฉ”๋‰ด) ํ˜น์€ ๊ธ€๋กœ๋ฒŒ ๋ ˆ๋ฒจ์—์„œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ํ”„๋กœ์ ํŠธ๋ฅผ SonarQube๋กœ ๋ถ„์„ํ•˜๊ณ  Artifactory๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๊ธ€๋กœ๋ฒŒ ๋ ˆ๋ฒจ์—์„œ ์›นํ›…์„ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ๋ณด๋‹ค ํŽธ๋ฆฌํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Artifactory ์‚ฌ์šฉ์ž ํ”Œ๋Ÿฌ๊ทธ์ธ

Artifactory Pro ๋ฐ Enterprise๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Groovy ์Šคํฌ๋ฆฝํŠธ๋กœ ์ž‘์„ฑํ•œ ์‚ฌ์šฉ์ž ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ์†์‰ฝ๊ฒŒ Artifactory์˜ ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ •๊ธฐ์ ์ธ ํƒœ์Šคํฌ(์˜ˆ. ํด๋ฆฐ์—… ๋“ฑ) ๋˜๋Š” ํŠน์ •ํ•œ ์ด๋ฒคํŠธ์™€ ์—ฐ๋™๋œ ์‚ฌ์šฉ์ž ์ •์˜ ํƒœ์Šคํฌ(์˜ˆ: ๋‹ค์šด๋กœ๋“œ ์‘๋‹ต์„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ํŠน์ •ํ•œ ๋ณด์•ˆ ์ •์ฑ…์„ ๋ณ€๊ฒฝํ•˜๋Š” ๋“ฑ)๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ๋ฌผ๋ก  ์ƒˆ๋กœ์šด API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋…ธ์ถœ์‹œํ‚ฌ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค(์˜ˆ, SonarQube ์›นํ›…์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” ํŠน์ •ํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋“ฑ).

๋‹ค์Œ ์ฝ”๋“œ ์Šค๋‹™ํŽซ์„ ๋ณผ๊นŒ์š”?

executions { //Expose a new endpoint for sonarqube webhook updateSonarTaskStatus(httpMethod: 'POST', users: ["admin"], groups: [], params:[targetRepo: '']) { params, ResourceStreamHandle body -> targetRepo = getStringProperty(params, 'targetRepo', true) bodyJson = new JsonSlurper().parse(body.inputStream) sonarTaskId = bodyJson.taskId //Implement your workflow based on SonarQube quality gate result } }

์œ„ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์ƒ์„ฑํ•œ ์ƒˆ๋กœ์šด ์—”๋“œํฌ์ธํŠธ๋Š” SonarQube ์›นํ›…์—์„œ ์•„๋ž˜ URL๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

http://admin:password@<ARTIFACTORY_URL>:8081/artifactory/api/plugins/execute/updateSonarTaskStatus?params=targetRepo=gradle-staging-local

Jenkins ํ™œ์šฉํ•˜๊ธฐ

์šฐ๋ฆฌ๋Š” Jenkins CI ์„œ๋ฒ„์— ์ปค๋ฐ‹ ๋นŒ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ด ์ปค๋ฏธํ„ฐ์—๊ฒŒ ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•˜๋ฉฐ, (์ด์ „ ํฌ์ŠคํŠธ์— ๊ธฐ์ˆ ํ–ˆ๋˜ ๊ฒƒ ์ฒ˜๋Ÿผ) ์†Œ๋‚˜ํ๋ธŒ ๋ถ„์„(taskId)๊ณผ Artifactory ๋นŒ๋“œ ์ •๋ณด(๋ฐ ๊ด€๋ จ ์‚ฐ์ถœ๋ฌผ)์„ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

์ „์ฒด์ ์ธ ๊ทธ๋ฆผ

Jenkins ์„œ๋ฒ„์—๋Š” ์ปค๋ฐ‹ ๋นŒ๋“œ๊ฐ€ ๋งค ์ปค๋ฐ‹์— ๋Œ€ํ•œ ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค(๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๊ฐ€ ๊ณต์šฉ ํ™˜๊ฒฝ์—์„œ ๋นŒ๋“œ๋˜์—ˆ๋Š”์ง€, ๋‚ด ์ฝ”๋“œ๊ฐ€ ๊ธฐ์กด ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ๋ง๊ฐ€๋œจ๋ฆฌ์ง€๋Š” ์•Š์•˜๋Š”์ง€, ๊ณ„์† ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋„ ๋˜๋Š”์ง€ ๋“ฑ).

์ปค๋ฐ‹ ๋นŒ๋“œ ์›Œํฌํ”Œ๋กœ์šฐ(commit build workflow)๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

SonarQube์˜ quality gate ๊ฒฐ๊ณผ๊ณผ ๊ด€๋ จ๋œ ๋กœ์ง๋“ค์€ ๋‘๋ฒˆ์งธ โ€œstagingโ€ ์›Œํฌํ”Œ๋กœ์šฐ์— ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ ์—ฌ๋Ÿฌ๋ถ„์€ ๋นŒ๋“œ๋ฅผ ๊ณ„์† ์ง„ํ–‰ํ•˜๊ฑฐ๋‚˜(ํ”„๋กœ๋ชจํŠธ) ๋ณด๋‹ค ๋†’์€ ์ˆ˜์ค€์˜ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ, ๋ฐฐํฌ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์™ธ๋ถ€ ๋„๊ตฌ๋“ค์„ ํŠธ๋ฆฌ๊ฑฐ๋ง ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์Šคํ…Œ์ด์ง• ์›Œํฌํ”Œ๋กœ์šฐ(staging workflow)๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

์‹œ๋„ํ•ด ๋ณด์‹ญ์‹œ์˜ค

SonarSource์™€ JFrog์˜ ํ˜‘์—…์ด ๊ฒฐ์‹ค์„ ๋งบ๊ณ  ์žˆ์–ด์„œ ๋งค์šฐ ํฅ๋ถ„๋ฉ๋‹ˆ๋‹ค. ์ด ํ†ตํ•ฉ ๊ณผ์ •์„ ์‹œ๋„ํ•ด๋ณด๊ณ  ์—ฌ๋Ÿฌ๋ถ„์ด ๋Š๋‚€ ๊ฒƒ๋“ค์„ ์•Œ๋ ค์ฃผ์‹ญ์‹œ์˜ค. Integration GitHub project์— ์ด์Šˆ๋ฅผ ์ƒ์„ฑํ•ด์„œ ์ €ํฌ์™€ ๋Œ€ํ™”๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ยฉ 2017-2018 Moses Kim.

๋ณ„๋„์˜ ์–ธ๊ธ‰์ด ์—†๋Š” ํ•œ, ์ด ์ŠคํŽ˜์ด์Šค์˜ ์ปจํ…์ธ ๋Š” ํฌ๋ฆฌ์—์ดํ‹ฐ๋ธŒ ์ปค๋จผ์ฆˆ ์ €์ž‘์žํ‘œ์‹œ-๋น„์˜๋ฆฌ-๋™์ผ์กฐ๊ฑด๋ณ€๊ฒฝํ—ˆ๋ฝ 4.0 ๊ตญ์ œ ๋ผ์ด์„ ์Šค์— ๋”ฐ๋ผ ์ด์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
SONARQUBE๋Š” SonarSource SA์˜ ํŠธ๋ ˆ์ด๋“œ ๋งˆํฌ์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ํŠธ๋ ˆ์ดํŠธ ๋งˆํฌ ๋ฐ ์ €์ž‘๊ถŒ์€ ๊ฐ ์†Œ์œ ์ž์˜ ์†Œ์œ ๋ฌผ์ž…๋‹ˆ๋‹ค.

::: SonarQube ๊ด€๋ จ ๋ฌธ์˜ : ์ด๋ฉ”์ผ :::