๋ฐ˜์‘ํ˜•

 

๋กœ์ปฌ์—์„œ Spring Boot ๋นŒ๋“œํ•˜๊ณ  docker image ๋งŒ๋“ค์–ด์„œ docker hub์— ์˜ฌ๋ฆฌ๊ณ ..
EC2 ๋“ค์–ด๊ฐ€์„œ docker hub์— ์žˆ๋Š” ์ด๋ฏธ์ง€ pull ๋ฐ›์•„์„œ run ์‹œํ‚ค๊ธฐ...

 

๊ฐœ๋ฐœ์„ ํ•˜๋ฉด ํ• ์ˆ˜๋ก CI/CD์˜ ์ค‘์š”์„ฑ์ด ๊นŠ๊ฒŒ ๋Š๊ปด์ง€๋Š” ๊ฒƒ ๊ฐ™๋‹ค... 
๋ฐฐํฌ์— ์žˆ์–ด์„œ ๋‚˜์™€ ๊ฐ™์ด ๊ท€์ฐฎ์Œ ํ˜น์€ ์–ด๋ ค์›€์„ ๋Š๋ผ๊ณ  ์žˆ๋Š” ๋‹ค๋ฅธ ๋ถ„๋“ค์„ ์œ„ํ•ด ๊ธฐ๋ก์„ ๋‚จ๊ธฐ๋ ค๊ณ ํ•œ๋‹ค

 

 

์‹œ์ž‘ํ•˜๊ธฐ์— ์•ž์„œ

 

์Šฌํ”ˆ ์‚ฌ์—ฐ (Jenkins ๋„์ž…๊ธฐ..)

 

์ผ๋‹จ ์‹œ์ž‘ํ•˜๊ธฐ์— ์•ž์„œ Jenkins์„ ๋„์ž…ํ•˜๊ธฐ๊นŒ์ง€์˜ ์Šฌํ”ˆ ์ „์„ค์ด ์žˆ๋‹ค..

 

https://hanjungyo.tistory.com/126

 

Github Actions ํ†บ์•„๋ณด๊ธฐ

Github Actions์˜ ๊ธฐ๋ณธ ๊ฐœ๋… ์ผ๋‹จ Github Actions๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ๋Š” CI/CD ๋„๊ตฌ์ž„=> ์ฝ”๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋นŒ๋“œ, ํ…Œ์ŠคํŠธ, ๋ฐฐํฌ ๋“ฑ์˜ ์ž‘์—…์„ ์ž๋™์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Œ Github

hanjungyo.tistory.com

 

์›๋ž˜ ์œ„ ๊ฒŒ์‹œ๊ธ€์˜ ์ด๋ฆ„์€ Github Actions๋ฅผ ์ด์šฉํ•œ Spring Boot ๋ฐฐํฌ ์ž๋™ํ™”์˜€๋‹ค...

 

Github Actions๋Š” GitHub์™€ ์—ฐ๋™๋˜๊ธฐ์— ๊ต‰์žฅํžˆ ํŽธ๋ฆฌํ•œ ๊ธฐ๋Šฅ๋“ค์ด ๋งŽ์•˜๊ณ  ๋‚œ์ด๋„๋„ ์ƒ๋Œ€์ ์œผ๋กœ ์‰ฌ์›Œ์„œ ์„ ํƒํ•˜๊ฒŒ ๋˜์—ˆ๋Š”๋ฐ

 

์—ด์‹ฌํžˆ ๊ธ€์„ ์“ฐ๋˜ ๋„์ค‘ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์˜(์กธ์—… ํ”„๋กœ์ ํŠธ) ๋ ˆํฌ์ง€ํ† ๋ฆฌ๊ฐ€ ๋Œ€ํ•™๊ต organization์—์„œ๋งŒ ์ƒ์„ฑ์„ ํ•ด์•ผํ•˜๋Š”๋ฐ Repo Setting ๊ถŒํ•œ์ด ๋ง‰ํ˜€์žˆ์Œ...

 

self-hosted runner ํ™˜๊ฒฝ์„ ์“ฐ์ž๋‹ˆ ๊ฒฐ๊ตญ EC2(t2.micro ์‚ฌ์šฉ์ค‘)์—์„œ ๋นŒ๋“œ๋ฅผ ๋Œ๋ ค์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด ์ƒ๊ธฐ๋Š”๋ฐ t2.micro์—์„œ ์‹œ๋„ํ•ด๋ณธ ๊ฒฐ๊ณผ ๋นŒ๋“œ๋ฅผ ํ•˜๋˜ ๋„์ค‘ CPU 100%์„ ๋šซ๊ณ  ๋ฌดํ•œ ๋กœ๋”ฉ์— ๊ฑธ๋ ค๋ฒ„๋ฆฌ๋Š” ๋ฆฌ์†Œ์Šค ์ด์Šˆ๊ฐ€ ํŒŒ์•…๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— Github Actions๋ฅผ ํฌ๊ธฐํ–ˆ๋‹ค
(์–ด์ฉŒํ”ผ ์›๋ณธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์—์„œ runner ๋“ฑ๋ก ๋ชปํ•˜๋ฏ€๋กœ self-hosted runner๋„ ๋ถˆ๊ฐ€๋Šฅ)

 

AWS Lambda + AWS CodeBuild + GitHub Actions๋กœ ๋ ˆํฌ์ง€ํ† ๋ฆฌ Setting์„ ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•˜๋Š” ์ƒํ™ฉ์„ ์šฐํšŒ? ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์ง€๋งŒ ๋น„์šฉ๊ณผ ๋„คํŠธ์›Œํฌ ์ง€์—ฐ ๋“ฑ์œผ๋กœ Jenkins๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์ข‹์€ ์„ ํƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•จ

 

=> Jenkins์—์„œ Git Plugin์œผ๋กœ ๋ชฉํ‘œ๋กœํ–ˆ๋˜ "Git ์ €์žฅ์†Œ์™€ ํ†ตํ•ฉํ•˜์—ฌ ํŠธ๋ฆฌ๊ฑฐ"๋ฅผ ํ•  ์ˆ˜ ์žˆ๊ณ  Circle CI, Travis CI ๊ฐ™์€ ๋‹ค๋ฅธ CI/CD ํˆด๋“ค๋„ ์‚ดํŽด๋ณด์•˜๋Š”๋ฐ ๋ฌด๋ฃŒ ํ”Œ๋žœ์˜ ํ•œ๊ณ„ ๋ฐ ๋น„์šฉ ์ด์Šˆ๋„ ์žˆ๊ธฐ์— ์˜คํ”ˆ์†Œ์Šค์ธ Jenkins๋ฅผ ์„ ํƒํ•จ

 

 

Jenkins๋ž€?

 

 

 

Jenkins๋Š” ์˜คํ”ˆ ์†Œ์Šค CI/CD ๋„๊ตฌ๋กœ, ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ž๋™ํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋จ

 

=> ๋‹ค์–‘ํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ง€์›ํ•˜์—ฌ ๋นŒ๋“œ, ๋ฐฐํฌ, ํ…Œ์ŠคํŠธ ๋“ฑ์˜ ์ž‘์—…์„ ์œ ์—ฐํ•˜๊ฒŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์—ฌ๋Ÿฌ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์™€ ๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ์ง€์›ํ•จ

 

ํŠน์ง•์„ ์‚ดํŽด๋ณด๋ฉด

 

  • ์•ฝ 1500๊ฐœ ์ด์ƒ์˜ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ†ตํ•ด ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์–ด, ํŒ€์˜ ์š”๊ตฌ ์‚ฌํ•ญ์— ๋งž๊ฒŒ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•  ์ˆ˜ ์žˆ์Œ

  • ํŒŒ์ดํ”„๋ผ์ธ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ฉด CI/CD ํ”„๋กœ์„ธ์Šค๋ฅผ ์ฝ”๋“œ๋กœ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Œ

    => ๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์—์„œ ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ

  • ์˜คํ”ˆ์†Œ์Šค์ด๊ธฐ์— ๋ฌด๋ฃŒ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ์ž˜ ํ™œ์„ฑํ™” ๋˜์–ด์žˆ์Œ

  • ์›น ๊ธฐ๋ฐ˜์˜ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์ œ๊ณตํ•˜๊ธฐ์— ๋นŒ๋“œ ์ƒํƒœ์™€ ๋กœ๊ทธ๋ฅผ ์‰ฝ๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ

 

Jenkins๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹จ์ผ ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜๊ธฐ์— ๋งŽ์€ ํ”Œ๋Ÿฌ๊ทธ์ธ๊ณผ ์ž‘์—…์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ๋ฆฌ์†Œ์Šค๋ฅผ ๋งŽ์ด ์†Œ๋ชจํ•˜๋ฉฐ ์ด๋กœ ์ธํ•ด ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Œ

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š”

Jenkins ์—์ด์ „ํŠธ๋ฅผ ์„ค์ •ํ•˜์—ฌ ์ž‘์—…์„ ๋ถ„์‚ฐ์‹œํ‚ค๊ฑฐ๋‚˜ AWS, GCP์™€ ๊ฐ™์€ ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ํ™•์žฅํ•˜๊ฑฐ๋‚˜, ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ๋ฏธ์น˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ตœ์†Œํ™”ํ•˜๊ณ  ํ•„์ˆ˜์ ์ธ ํ”Œ๋Ÿฌ๊ทธ์ธ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Œ

 

 

Jenkins์„ ์ด์šฉํ•œ Spring Boot ๋ฐฐํฌ ์ž๋™ํ™”

 

 

์›๋ž˜ EC2์— Jenkins ์„œ๋ฒ„๋ฅผ ์‹คํ–‰์‹œ์ผœ๋†“๊ณ  ํ•˜๋ ค๊ณ ํ–ˆ๋Š”๋ฐ ๊ฐœ๋ฐœ ์ดˆ๊ธฐ ๋‹จ๊ณ„์ด๊ณ  ํŒ€ ์ธ์›๋„ ์ ๊ธฐ์—

๋น„์šฉ์„ ์ตœ๋Œ€ํ•œ ์ ˆ์•ฝํ•˜๊ณ ์ž ๋กœ์ปฌ์—์„œ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๋˜๋ฉด CD๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋กœ์ปฌ์—์„œ jenkins ์„œ๋ฒ„๋ฅผ ํ‚ค๊ณ  ์žˆ์–ด์•ผ ํ•˜๊ธฐ์— ์ธ์› ํ•œ ๋ช…์ด ์ด๋ฅผ ๋‹ด๋‹นํ•ด์•ผํ•จ
ํ•˜์ง€๋งŒ, ์œ„์—์„œ ๋งํ•œ๋Œ€๋กœ ์ธ์›๋„ ์ ๊ณ  ๊ฐœ๋ฐœ ์ดˆ๊ธฐ ๋‹จ๊ณ„๋ผ ์•„์ง ๋นˆ๋ฒˆํ•˜๊ฒŒ merge๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ธฐ์— ๋กœ์ปฌ์—์„œ jenkins๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ๋น„์šฉ์„ ์ตœ๋Œ€ํ•œ ์ ˆ์•ฝํ•˜๋‹ค๊ฐ€ ์ถ”ํ›„ MVP(Minimum Viable Product)๊ฐ€ ์™„์„ฑ๋˜๋ฉด์„œ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„์ด ๋นˆ๋ฒˆํ•ด์งˆ ๋•Œ EC2 ์ƒ์— ์˜ฌ๋ฆฌ๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝ ์˜ˆ์ •

 

EC2์—์„œ Jenkins๋ฅผ ๋„์šฐ๊ฒŒ ๋  ๋•Œ ๊ด€๋ จ ๋‚ด์šฉ๋„ ๊ธฐ๋กํ•ด๋‘๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
(๋ฐฉ์‹์€ ๊ฑฐ์˜ ์œ ์‚ฌํ•จ)

 

Mac์—์„œ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹คโ€ผ๏ธ

 

1. Jenkins ์„ค์น˜ ๋ฐ ์„ค์ •

 

ํ„ฐ๋ฏธ๋„์—์„œ brew install jenkins-lts ๋ช…๋ น์–ด ์ž…๋ ฅ

 

=> Homebrew๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Jenkins๋ฅผ ์„ค์น˜
(์‹œ๊ฐ„์ด ์ข€ ์†Œ์š”๋˜๊ณ  Java๋กœ ์ž‘์„ฑ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— JDK๋ฅผ ๋จผ์ € ์„ค์น˜ํ•˜๋ฉด ์ข‹์Œ
-> Homebrew์—์„œ ์ž๋™์œผ๋กœ OpenJDK๋ฅผ ์„ค์น˜ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ๋งŽ๋‹ค๊ณ ํ•จ)

 

 

localhost:8080

 

brew services start jenkins-lts ๋ช…๋ น์–ด๋กœ Jenkins ์„œ๋น„์Šค๋ฅผ ์‹œ์ž‘ํ•˜๊ณ 

์›น ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์—ด์–ด http://localhost:8080 ์œผ๋กœ ์ ‘์†ํ•˜๋ฉด Jenkins์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Œ

 

cat /Users/$(whoami)/.jenkins/secrets/initialAdminPassword

 

์œ„ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด  ์ดˆ๊ธฐ ์•”ํ˜ธ๋ฅผ ์ฐพ์€ ํ›„ ์ž…๋ ฅํ•ด์ค˜์•ผํ•จ

 

 

 

๊ทธ ํ›„ ์ดˆ๊ธฐ ์„ธํŒ…(ํ”Œ๋Ÿฌ๊ทธ์ธ)์„ ํ•ด์ค˜์•ผํ•˜๋Š”๋ฐ ์œ„์—์„œ ๋งํ–ˆ๋“ฏ์ด ํ•„์š”ํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ๋งŒ ์„ค์น˜ํ•˜๋Š”๊ฒŒ ์„ฑ๋Šฅ๋ฉด์—์„œ ๋” ์ข‹์€ ์„ ํƒ์ด์ง€๋งŒ ์ด์ œ ๋ง‰ ์ฒ˜์Œ ์จ๋ณด๋Š” ๋‹จ๊ณ„์ด๊ธฐ์— ์ผ๋‹จ suggested๋กœ ์„ค์น˜๋ฅผ ํ•˜๊ณ  Git๊ณผ Docker๋งŒ ์ถ”๊ฐ€ ์„ค์น˜๋ฅผ ํ•ด์คฌ์Œ

(Dashboard -> Jenkins ๊ด€๋ฆฌ -> Plugins์—์„œ ์ถ”ํ›„ ์„ค์น˜ ๊ฐ€๋Šฅ)

 

 

์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ๊ณ„์ •์„ ์ƒ์„ฑํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ๋„˜์–ด๊ฐ€๋Š”๋ฐ ๊ณ„์ • ์ƒ์„ฑ ๋ฐ URL์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Œ

 

2. SSH ์„ค์ •

 

SSH ์ธ์ฆ ๋ฐฉ์‹

 

SSH(Secure Shell)์€ ๋น„๋Œ€์นญ ์•”ํ˜ธํ™” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์›๊ฒฉ ์„œ๋ฒ„์— ์ ‘์†ํ•˜๋Š” ํ”„๋กœํ† ์ฝœ์ž„

 

=> ๊ณต๊ฐœํ‚ค์™€ ํ”„๋ผ์ด๋น— ํ‚ค๋กœ ์ด๋ฃจ์–ด์ง„ ํ‚ค ์Œ์„ ์‚ฌ์šฉ

 

 

ํด๋ผ์ด์–ธํŠธ(์ ‘์†ํ•˜๋Š” ์ชฝ)๊ฐ€ private key(id_rsa)๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  ์„œ๋ฒ„(์ ‘์† ๋ฐ›๋Š” ์ชฝ)์ด public key(id_rsa.pub)์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์•ผ ํ†ต์‹ ์ด ๊ฐ€๋Šฅํ•จ

ํ˜„์žฌ ์ƒํ™ฉ์—์„œ ์ƒ๊ฐํ•ด๋ณด๋ฉด Jenkins์—์„œ GitHub์™€ EC2์— ์ ‘๊ทผํ•˜์—ฌ CD์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋ฏ€๋กœ

 

  • ํ”„๋ผ์ด๋น— ํ‚ค (id_rsa) โ†’ Jenkins์ด ๊ฐ€์ง€๊ณ  ์žˆ์–ด์•ผ ํ•จ

  • ๊ณต๊ฐœ ํ‚ค (id_rsa.pub) โ†’ ์›๊ฒฉ ์„œ๋ฒ„(GitHub, EC2)์˜ ~/.ssh/authorized_keys์— ์ €์žฅ๋˜์–ด ์žˆ์–ด์•ผ ํ•จ

 

ssh-keygen -t rsa -b 4096 -C "github ์ด๋ฉ”์ผ"

 

์šฐ์„  ๋กœ์ปฌ ํ„ฐ๋ฏธ๋„์—์„œ ์œ„ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด SSHํ‚ค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  id_rsa(ํ”„๋ผ์ด๋น— ํ‚ค) ์™€ id_rsa.pub(ํผ๋ธ”๋ฆญ ํ‚ค) ๊ฐ’์„ ๋ณต์‚ฌํ•จ

(~/.ssh ์— ์กด์žฌ)

Jenkins์—์„œ SSHํ‚ค ์ถ”๊ฐ€

 

์ด์ œ Jenkins์— SSHํ‚ค(ํ”„๋ผ์ด๋น— ํ‚ค)๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผํ•จ

 

(์ขŒ) Credentials ํด๋ฆญ ํ›„ ํ™”๋ฉด, (์šฐ) System ํด๋ฆญ ํ›„ ํ™”๋ฉด

 

 

Jenkins UI์—์„œ ํ”„๋กœํ•„์„ ํด๋ฆญํ•˜๊ณ  Credentials ํด๋ฆญ -> System ํด๋ฆญ -> Global credentials์—์„œ Add Credentials ํด๋ฆญ

 

 

ID๋Š” ssh-key๋กœ ํ•˜์˜€์Œ (์‚ฌ์ง„์—๋Š” github-ssh-key๋ผ๊ณ  ๋˜์–ด์žˆ์Œ)

 

 

Kind๋ฅผ SSH Username with private key๋กœ ์„ค์ •ํ•˜๊ณ  Username์œผ๋กœ git์ด๋‚˜ GitHub ์‚ฌ์šฉ์ž์ด๋ฆ„์„ ์ ์–ด์ฃผ๊ณ  Private Key์—์„œ Enter directly๋กœ ๋ณต์‚ฌํ•œ SSH ๊ฐœ์ธ ํ‚ค๋ฅผ ๋„ฃ์–ด์คŒ (~/.ssh/id_rsa ์˜ ๋‚ด์šฉ์„ ๋„ฃ์–ด์คŒ)

 

GitHub์—์„œ  SSH ํ‚ค ์ถ”๊ฐ€

 

์œ„์—์„œ sshํ‚ค๋ฅผ ๋งŒ๋“œ๋Š” ๋ช…๋ น์–ด๋ฅผ ์ž˜ ์ˆ˜ํ–‰ํ–ˆ๋‹ค๋ฉด ๋กœ์ปฌ์˜ ~/.ssh/id_rsa.pub ์— ๊ณต๊ฐœํ‚ค๊ฐ€ ๋‹ด๊ฒจ์ ธ์žˆ์„ํ…๋ฐ

ํ•ด๋‹น ๋‚ด์šฉ์„ cat ๋ช…๋ น์–ด๋“ฑ์œผ๋กœ ํ™•์ธ ํ›„ ๋ณต์‚ฌํ•˜์—ฌ

 

 

GitHub ๊ณ„์ •์˜ Settings -> SSH and GPG keys๋กœ ์ด๋™ํ•˜์—ฌ ์ƒˆ๋กœ์šด SSH ํ‚ค๋ฅผ ์ถ”๊ฐ€ํ•ด์คŒ 

 

EC2์—์„œ SSHํ‚ค ์ถ”๊ฐ€

 

 

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ~/.ssh ๋กœ ์ด๋™ ํ›„ vi authorized_keys ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด authorized_keys์— ๋กœ์ปฌ์—์„œ ๋งŒ๋“  SSHํ‚ค (id_rsa.pub)์˜ ๋‚ด์šฉ์„ ๋„ฃ์–ด์คŒ

 

 

3. Jenkins ํŒŒ์ดํ”„๋ผ์ธ ์ƒ์„ฑ

 

 

 

Jenkins ๋Œ€์‹œ๋ณด๋“œ์—์„œ New Item์„ ํด๋ฆญํ•œ ํ›„ 2๋ฒˆ์งธ์— ์žˆ๋Š” Pipeline์„ ์„ ํƒ

 

4. Poll SCM ์„ค์ • (GitHub Webhook ์‚ฌ์šฉ ๋ถˆ๊ฐ€ ์ด์Šˆ์˜ ๋Œ€์•ˆ)

 

๋งจ์œ„์—์„œ ๋งํ–ˆ๋“ฏ์ด ํ˜„์žฌ fork๋œ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์—์„œ๋งŒ ์ž‘์—…ํ•ด์•ผํ•œ๋‹ค๋Š” ์ œ์•ฝ์ด ๊ฑธ๋ ค ๋ ˆํฌ์ง€ํ† ๋ฆฌ์˜ Setting์„ ๊ฑด๋“ค ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ์ด๋ผ
GitHub Webhook ์ถ”๊ฐ€ ํ›„ ํŠน์ • ๋ธŒ๋ Œ์น˜์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ „๋‹ฌ๋ฐ›์•„ CD๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์—†๋Š” ์ƒํƒœ์ž„

=> ๋Œ€์•ˆ์œผ๋กœ ์ƒ๊ฐํ•œ ๋ฐฉ์‹์ด Poll SCM์„ ์„ค์ •ํ•˜์—ฌ ํŠน์ • ์ฃผ๊ธฐ(์‹œ๊ฐ„)๋งˆ๋‹ค Git ์ €์žฅ์†Œ๋ฅผ ํ™•์ธํ•˜๋ฉฐ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์žˆ์œผ๋ฉด ์ž‘์—…์„ ํ•˜๋Š” ๊ฒƒ์ž„

์‚ฌ์‹ค Poll SCM์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ํ•˜๊ฒŒ๋˜๋ฉด ๋ณ€๊ฒฝ์ด ์—†๋”๋ผ๋„ ์ผ์ • ์ฃผ๊ธฐ๋งˆ๋‹ค Git ์ €์žฅ์†Œ๋ฅผ ํ™•์ธํ•˜๋ฏ€๋กœ ๋ถˆํ•„์š”ํ•œ ์š”์ฒญ์ด ๋งŽ์„ ์ˆ˜ ๋ฐ–์— ์—†๊ณ  ์ •ํ•ด์ง„ ์ฃผ๊ธฐ๋งŒํผ์˜ ๋”œ๋ ˆ์ด๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Œ(ํŠนํžˆ ๋งŽ์€ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌํ•˜๋ฉด ์„ฑ๋Šฅ์ด...)

ํ•˜์ง€๋งŒ ๋ถˆํ•„์š”ํ•œ ์š”์ฒญ์˜ ๊ฒฝ์šฐ ๋กœ์ปฌ์—์„œ CD ๋™์ž‘์„ ํ•ด์•ผํ•  ๋•Œ๋งŒ Jenkins์„ ์ผœ์„œ ํ™œ์šฉํ•  ๊ฒƒ์ด๊ธฐ์— ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ณ  ๋”œ๋ ˆ์ด์˜ ๊ฒฝ์šฐ๋„ ํ˜„์žฌ๋Š” ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ๋‹จ๊ณ„์ด๊ธฐ์— ๊ฐ์ˆ˜ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Œ(์ผ๋‹จ ๋‹น์žฅ์€ ์ˆ˜๋™ ๋ฐฐํฌ์˜ ๊ท€์ฐฎ์Œ์„ ํ•ด์†Œํ•˜๋Š” ์ •๋„๋กœ๋งŒ ์‚ฌ์šฉ)

=> ๊ฐ€์žฅ ํ˜„์‹ค์ ์œผ๋กœ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์•ˆ์ด Poll SCM!!

 

 

Jenkins ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์œ„์—์„œ ๋งŒ๋“  Pipeline Job์„ ํด๋ฆญํ•ด์ฃผ๊ณ 

 

์™ผ์ชฝ ๊ตฌ์„ฑ(Configure) ํด๋ฆญ ํ›„ Triggres ํด๋ฆญ

 

 

 

Poll SCM ์„ ํƒ ํ›„ H/5 * * * * ์™€ ๊ฐ™์ด ์›ํ•˜๋Š” ์ฃผ๊ธฐ ์„ ํƒ

=> Ignore post-commit hooks๋Š” git commit, push ๋“ฑ์˜ ํŠน์ • Webhook ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ๋ฌด์‹œํ•˜๋„๋ก ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ž„
(์–ด์ฉŒํ”ผ Webhook ์„ค์ •์„ ๋ชปํ•˜๊ธฐ์— ์ด๋ฒคํŠธ๊ฐ€ ์˜ฌ๋ฆฌ ์—†์ง€๋งŒ ํ™•์‹คํ•˜๊ฒŒ ์ฒดํฌ!)

 

H/5 * * * * ๋Š” cron ํ‘œํ˜„์‹์œผ๋กœ ์ˆœ์„œ๋Œ€๋กœ MIN / HOUR / DAY / MONTH / DAT_OF_WEEK๋ฅผ ์˜๋ฏธํ•จ

=> H(ํ•ด์‹œ) ๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ 5๋ถ„๋งˆ๋‹ค ์‹คํ–‰๋จ
(*์€ ์™€์ผ๋“œ์นด๋“œ๋กœ ๋งค์‹œ๊ฐ„, ๋งค์ผ, ๋งค์›”, ๋งค์š”์ผ ์‹คํ–‰๋˜๋ฏ€๋กœ)

์ฐธ๊ณ ๋กœ 0/5 ์™€ H/5์˜ ์ฐจ์ด๋Š”

H๋Š” Jenkins์˜ ํ•ด์‹œ ๊ฐ’์œผ๋กœ ํŠน์ • ์‹œ๊ฐ„์— ๋นŒ๋“œ๊ฐ€ ๋ชฐ๋ฆฌ๋Š” ๊ฑธ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ Job๋งˆ๋‹ค ๋‹ค๋ฅธ ์‹œ์ž‘ ์‹œ๊ฐ„์„ ์ž๋™์œผ๋กœ ๋ฐฐ์ •ํ•ด์คŒ
(๊ฐ Jenkins Job์ด ๋™์ผํ•œ ์‹œ๊ฐ์— ์‹คํ–‰๋˜์ง€ ์•Š๊ณ  ๋ถ„์‚ฐํ•ด์„œ ์‹คํ–‰) 

0/5๋Š” ์ •ํ™•ํžˆ 0, 5, 10 ... ๋ถ„๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š”๊ฑฐ๋ผ ๋ชจ๋“  Job์ด ๊ฐ™์€ ์‹œ๊ฐ์— ์‹คํ–‰๋จ

 

 

5. Jenkins์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • (+ Docker Hub credentials ์„ค์ •)

 

 

 

Jenkins ๋Œ€์‹œ๋ณด๋“œ์—์„œ Manage Jenkins(Jenkins ๊ด€๋ฆฌ) -> System -> Global properties ์„น์…˜์—์„œ Environment variables๋ฅผ ์ฒดํฌํ•œ ํ›„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Œ

 

  • GITHUB_BRANCH : ๋นŒ๋“œ์™€ ๋ฐฐํฌ์— ์‚ฌ์šฉ๋  GitHub ์ €์žฅ์†Œ์˜ ๋ธŒ๋žœ์น˜

    => backend/main

  • GITHUB_REPO_URL : GitHub ์ €์žฅ์†Œ URL

    (HTTPS๋ง๊ณ  SSH URL์„ ๊ฐ€์ ธ์™€์•ผํ•จ)
  • GRADLE_TASK : Gradle ๋นŒ๋“œ ์‹œ ์‹คํ–‰ํ•  ํƒœ์Šคํฌ

    => clean bootJar

  • DOCKER_BUILD_PLATFORMS : Docker ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•  ๋Œ€์ƒ ํ”Œ๋žซํผ

    => linux/amd64,linux/arm64

  • DOCKER_IMAGE_NAME : ๋นŒ๋“œ๋œ Docker ์ด๋ฏธ์ง€์˜ ์ด๋ฆ„๊ณผ ํƒœ๊ทธ

    => hjungyo/2025capstone:latest ์™€ ๊ฐ™์ด dockerhub-username/your-app:version ์˜ ํ˜•ํƒœ

  • EC2_HOST : ๋ฐฐํฌ ๋Œ€์ƒ EC2 ์ธ์Šคํ„ด์Šค์˜ ํผ๋ธ”๋ฆญ IP

  • EC2_USER : EC2 ์ธ์Šคํ„ด์Šค์— SSH ์ ‘์†ํ•  ๋•Œ ์‚ฌ์šฉํ•  ์‚ฌ์šฉ์ž ์ด๋ฆ„

    => ubuntu (linux๋ผ๋ฉด ec2-user)

  • CONTAINER_PORT : Docker ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์‚ฌ์šฉํ•  ํฌํŠธ ๋ฒˆํ˜ธ

    => 8080

 

์ด๋ ‡๊ฒŒ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •์„ ๋งˆ์น˜๋ฉด ๋์ด ์•„๋‹˜!

๋กœ์ปฌ์—์„œ ๋นŒ๋“œํ•˜์—ฌ ๋งŒ๋“  ์ด๋ฏธ์ง€๋ฅผ Docker Hub์— ์˜ฌ๋ฆฐ ํ›„ ์˜ฌ๋ผ๊ฐ„ ์ด๋ฏธ์ง€๋ฅผ EC2์—์„œ ๊ฐ€์ ธ์™€ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋„์šฐ๋ฏ€๋กœ ๊ด€๋ จ ์„ค์ •์„ ํ•ด์ค˜์•ผํ•จ
=> Docker Hub ์„ค์ •

 

 

์œ„์—์„œ sshํ‚ค๋ฅผ ๋“ฑ๋กํ–ˆ๋˜๋Œ€๋กœ credentials ์„ค์ •์—์„œ docker hub ์•„์ด๋””๋ฅผ Username ํ•„๋“œ์—, ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ Password์— ์ž…๋ ฅ ํ›„ Create ํ•˜๋ฉด ๋จ

 

ID๊ฐ™์€ ๊ฒฝ์šฐ๋Š” ์•Œ์•„๋ณผ ์ˆ˜ ์žˆ๊ฒŒ dockerhub-credentials๋ผ๊ณ  ๋ช…์‹œํ•˜์˜€์Œ

 

6. Jenkins Pipeline Script ์ž‘์„ฑ

 

์ตœ์ข… Script๋Š” ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… ๋ถ€๋ถ„ ์ฐธ๊ณ โ€ผ๏ธ

 

 

Jenkins๋Š” Groovy๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ CI/CD ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•จ => Groovy๋Š” JVM์—์„œ ์‹คํ–‰๋˜๋Š” ๊ฐ์ฒด ์ง€ํ–ฅ ์–ธ์–ด์ž„

 

 

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์•„๊นŒ ๋งŒ๋“  ํŒŒ์ดํ”„๋ผ์ธ์˜ ๊ตฌ์„ฑ(Configure) ํด๋ฆญ ํ›„ Pipeline์„ ํด๋ฆญํ•˜๋ฉด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Œ 

 

๋ฐ‘์— ์ฒดํฌ๋œ Use Groovy Sandbox๋Š” Jenkins Pipeline์—์„œ Groovy ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ์˜ต์…˜์ž„


=> Sandbox ํ™˜๊ฒฝ์€ ์ฝ”๋“œ ์‹คํ–‰์ด ์ œํ•œ๋œ ๊ฒฉ๋ฆฌ๋œ ํ™˜๊ฒฝ์„ ์˜๋ฏธํ•จ
(ํŠน์ • ๋ฉ”์„œ๋“œ๋‚˜ ํด๋ž˜์Šค์˜ ์‚ฌ์šฉ์ด ์ œํ•œ๋˜์–ด ์œ„ํ—˜ํ•œ ์ž‘์—…์„ ๋ฐฉ์ง€, ์•…์˜์ ์ธ ์ฝ”๋“œ ์‹คํ–‰ ๋ฐฉ์ง€ ๋“ฑ)

 

 

์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๋ฉด

 

pipeline {
    agent any
    
    stages {
        stage('Checkout') {
            steps {
                git branch: env.GITHUB_BRANCH, url: env.GITHUB_REPO_URL
            }
        }
        
        stage('Build') {
            steps {
                dir('backend') {
                    sh "./gradlew ${env.GRADLE_TASK}"
                }
            }
        }
        
        stage('Build and Push Docker Image') {
            steps {
                dir('backend') {
                    withCredentials([usernamePassword(credentialsId: 'dockerhub-credentials', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
                        sh """
                            echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin
                            docker buildx build --platform ${env.DOCKER_BUILD_PLATFORMS} -t ${env.DOCKER_IMAGE_NAME} --push .
                        """
                    }
                }
            }
        }
        
        stage('Deploy to EC2') {
            steps {
                sshagent(credentials: ['ssh-key']) {
                    sh """
                        ssh -o StrictHostKeyChecking=no ${env.EC2_USER}@${env.EC2_HOST} '
                            sudo docker stop \$(sudo docker ps -q --filter ancestor=${env.DOCKER_IMAGE_NAME})
                            sudo docker rm \$(sudo docker ps -aq --filter ancestor=${env.DOCKER_IMAGE_NAME})
                            sudo docker pull ${env.DOCKER_IMAGE_NAME}
                            sudo docker run --platform linux/amd64 -d -p ${env.CONTAINER_PORT}:${env.CONTAINER_PORT} ${env.DOCKER_IMAGE_NAME}
                        '
                    """
                }
            }
        }
    }
}

 

 

 

  • ๊ฐ stage๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋˜๋ฏ€๋กœdir('backend')๋ฅผ ํ†ตํ•ด ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ด๋™ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Œ

    => ํ˜„์žฌ GitHub์—๋Š” backend์™€ front ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ํ•จ๊ป˜ ์กด์žฌํ•˜๊ธฐ์— backend ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™ ํ›„ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด์•ผํ•จ


  • EC2 ๋ฐฐํฌ ๋ถ€๋ถ„์„ ์‚ดํŽด๋ณด๋ฉด
     
    • SSH ๋ช…๋ น์„ ์‚ฌ์šฉํ•˜์—ฌ EC2 ์ธ์Šคํ„ด์Šค์— ์—ฐ๊ฒฐํ•˜๊ณ ์žˆ๋Š”๋ฐ -o StrictHostKeyChecking=no ์˜ต์…˜์˜ ๊ฒฝ์šฐ ์ดˆ๊ธฐ ์—ฐ๊ฒฐ ์‹œ ํ˜ธ์ŠคํŠธ ํ‚ค ํ™•์ธ์„ ๊ฑด๋„ˆ๋›ฐ๋Š” ๊ฒƒ์ž„

      (ํ˜ธ์ŠคํŠธ ํ‚ค๋Š” SSH ์„œ๋ฒ„์˜ ๊ณ ์œ  ์‹๋ณ„์ž์ž„ -> ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐํ•˜๋ ค๋Š” ์„œ๋ฒ„์˜ ์‹ ์›์„ ํ™•์ธํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋จ)

      ๋”๋ณด๊ธฐ
      "Are you sure you want to continue connecting (yes/no)?" ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฐฉ์ง€

      => ๋ณด์•ˆ์„ ์œ„ํ•ด known_hosts ํŒŒ์ผ์„ ๊ด€๋ฆฌํ•˜๋Š”๊ฒŒ ๋” ์ข‹์Œ
      (๋ณด์•ˆ ๊ณ„์ธต ์ถ”๊ฐ€)


    • sshagent๋Š” Jenkins ํŒŒ์ดํ”„๋ผ์ธ์—์„œ SSH ์—ฐ๊ฒฐ์„ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋Šฅ์ธ๋ฐ ์ด๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” SSH Agent ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค์น˜ํ•ด์ค˜์•ผํ•จ!

ํ˜ธ์ŠคํŠธํ‚ค ํ™•์ธ์€ ์—ฐ๊ฒฐํ•˜๋ ค๋Š” ์„œ๋ฒ„๊ฐ€ ์‹ค์ œ๋กœ ์˜๋„ํ•œ ์„œ๋ฒ„์ธ์ง€ ํ™•์ธํ•˜๋Š” ๊ณผ์ •์ด๋ผ๊ณ  ๋งํ–ˆ๋Š”๋ฐ
์ด๋ฅผ ๊ฑด๋„ˆ๋›ฐ๋ฉด ๊ณต๊ฒฉ์ž๊ฐ€ ์ค‘๊ฐ„์—์„œ ํ†ต์‹ ์„ ๊ฐ€๋กœ์ฑ„๊ฑฐ๋‚˜ ์œ„์กฐ๋œ ์„œ๋ฒ„๋กœ ์—ฐ๊ฒฐ์„ ์œ ๋„ํ•  ์œ„ํ—˜์ด ์žˆ์Œ
(Man-in-the-Middle ์ด๋ผ๋Š” ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ)

=> SSHํ‚ค ์—†์œผ๋ฉด ์–ด์ฉŒํ”ผ ์˜๋ฏธ๊ฐ€ ์—†์ง€ ์•Š๋‚˜? ๋ผ๊ณ  ์ƒ๊ฐ์„ ํ–ˆ๋Š”๋ฐ ์ฐพ์•„๋ณด๋‹ˆ

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

 

 

known_hosts ํŒŒ์ผ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํŒŒ์ดํ”„๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ ๋ณ€๊ฒฝ

 

pipeline {
    agent any
    
    stages {
        stage('Checkout') {
            steps {
                git branch: env.GITHUB_BRANCH, url: env.GITHUB_REPO_URL
            }
        }
        
        stage('Build') {
            steps {
                dir('backend') {
                    sh "./gradlew ${env.GRADLE_TASK}"
                }
            }
        }
        
        stage('Build and Push Docker Image') {
            steps {
                dir('backend') {
                    withCredentials([usernamePassword(credentialsId: 'dockerhub-credentials', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
                        sh """
                            echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin
                            docker buildx build --platform ${env.DOCKER_BUILD_PLATFORMS} -t ${env.DOCKER_IMAGE_NAME} --push .
                        """
                    }
                }
            }
        }
        
        stage('Update Known Hosts') {
            steps {
                sh """
                    mkdir -p ~/.ssh
                    ssh-keyscan -H ${env.EC2_HOST} >> ~/.ssh/known_hosts
                """
            }
        }
        
        stage('Deploy to EC2') {
            steps {
                sshagent(credentials: ['ssh-key']) {
                    sh """
                        ssh ${env.EC2_USER}@${env.EC2_HOST} '
                            sudo docker stop \$(sudo docker ps -q --filter ancestor=${env.DOCKER_IMAGE_NAME})
                            sudo docker rm \$(sudo docker ps -aq --filter ancestor=${env.DOCKER_IMAGE_NAME})
                            sudo docker pull ${env.DOCKER_IMAGE_NAME}
                            sudo docker run --platform linux/amd64 -d -p ${env.CONTAINER_PORT}:${env.CONTAINER_PORT} ${env.DOCKER_IMAGE_NAME}
                        '
                    """
                }
            }
        }
    }
}

 

 

Update Known Hosts๋ผ๋Š” ์Šคํ…Œ์ด์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€๊ณ  
ssh-keyscan ๋ช…๋ น์„ ์‚ฌ์šฉํ•˜์—ฌ EC2 ํ˜ธ์ŠคํŠธ์˜ SSH ํ‚ค๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ ์žˆ์Œ

 

=> ๊ฐ€์ ธ์˜จ ํ‚ค๋ฅผ ~/.ssh/known_hosts ํŒŒ์ผ์— ์ถ”๊ฐ€

 

(๊ธฐ์กด์˜ StrictHostKeyChecking=no ์˜ต์…˜์€ ์ œ๊ฑฐํ•˜์˜€์Œ)

 

์ž‘๋™ ๋ฐฉ์‹์„ ์‚ดํŽด๋ณด๋ฉด

ssh-keyscan์œผ๋กœ ๊ฐ€์ ธ์˜จ ํ˜ธ์ŠคํŠธ ํ‚ค๋Š” ~/.ssh/known_hosts ํŒŒ์ผ์— ์ €์žฅ๋˜๊ณ 

SSH ์—ฐ๊ฒฐ์‹œ, ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํ˜ธ์ŠคํŠธ ํ‚ค๋ฅผ know_hosts ํŒŒ์ผ์˜ ํ‚ค์™€ ๋น„๊ตํ•จ
=> ํ‚ค๊ฐ€ ์ผ์น˜ํ•˜๋ฉด ์—ฐ๊ฒฐ์„ ์ง„ํ–‰ํ•˜๊ณ , ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฒฝ๊ณ ๋ฅผ ํ‘œ์‹œ

(์ด๋ฅผ ํ†ตํ•ด ์ดˆ๊ธฐ ์—ฐ๊ฒฐ ์‹œ ํ˜ธ์ŠคํŠธ ํ‚ค๋ฅผ ์ž๋™์œผ๋กœ ์ €์žฅํ•˜๊ณ , ์ดํ›„ ์—ฐ๊ฒฐ์—์„œ๋Š” ์ €์žฅ๋œ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„์˜ ์‹ ์›์„ ํ™•์ธํ•จ)

 

ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

 

 

 

ํ•ด์น˜์› ๋‚˜..? ๋ผ๊ณ  ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค๋ฉด ๋ญ”๊ฐ€๋ฅผ ๋†“์น˜๊ณ  ์žˆ๋‹ค๋Š” ๋œป!

 

1. ๊นŒ๋จน์€ application.yaml ํŒŒ์ผ... (with Permission denied)

 

 

์šฐ์„  ์œ„์—์„œ ์„ค์ •ํ•˜๋˜๋Œ€๋กœ Jenkins ๊ด€๋ฆฌ > Credentials > System > Global credentials ๋กœ ์ด๋™ ํ›„

kind๋ฅผ 'Secret file'๋กœ ํ•˜์—ฌ ๋กœ์ปฌ์—์„œ ์‚ฌ์šฉ์ค‘์ธ application.yaml์„ ์˜ฌ๋ ค์ฃผ์—ˆ์Œ
(ํ˜„์žฌ ๊นƒํ—ˆ๋ธŒ์—๋Š” application.yaml.example๋งŒ ์˜ฌ๋ผ๊ฐ€์žˆ๊ธฐ์— ๋กœ์ปฌ์—์„œ ์‚ฌ์šฉํ•˜๋Š” yaml์„ ์ฃผ์ž…ํ•˜๊ธฐ ์œ„ํ•ด)

 

        stage('Inject Configuration') {
            steps {
                withCredentials([file(credentialsId: 'application.yaml', variable: 'APP_YAML')]) {
                    sh 'cp -f $APP_YAML backend/src/main/resources/application.yaml'
                }
            }
        }

 

๋‹ค์Œ๊ณผ ๊ฐ™์ด GitHub์—์„œ Checkout ํ›„์— application.yaml์„ ์ถ”๊ฐ€ํ•˜๋Š” stage๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ์Œ

 

์ด ๋•Œ, cp ๋ช…๋ น์–ด์— -f ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ permission denied๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ๊ฐ•์ œ๋กœ ํŒŒ์ผ์„ ๋ฎ์–ด์“ฐ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค

 

 

2. No credentials specified

 

์ฒซ๋ฒˆ ์งธ stage์ธ Checkout ๋ถ€๋ถ„์—์„œ ์ž๊พธ No credentials specified ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Œ

 

=> Git ์ €์žฅ์†Œ์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ ์ธ์ฆ ์ •๋ณด๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Œ์„ ๋‚˜ํƒ€๋ƒ„

 

ํ”„๋ผ์ด๋น— ์ €์žฅ์†Œ์˜ ๊ฒฝ์šฐ ์ ์ ˆํ•œ SSH ํ‚ค๋‚˜ ์ธ์ฆ ์ •๋ณด๊ฐ€ ํ•„์š”ํ•˜๊ธฐ์— ์ด์ „์— GitHub์— ํ•จ๊ป˜ ์„ค์ •ํ•ด๋‘” ssh-key๋ฅผ Script์— ๋ช…์‹œํ•ด์คฌ์Œ

 

    stages {
        stage('Checkout') {
            steps {
                git branch: env.GITHUB_BRANCH, url: env.GITHUB_REPO_URL, credentialsId: 'ssh-key'
            }
        }

 

๋‹ค์Œ๊ณผ ๊ฐ™์ด credentialsId์— ssh-key๋ฅผ ๋ช…์‹œํ•ด์คฌ์Œ

 

3. gradlew Permission denied

 

์œ„ ์—๋Ÿฌ ๊ฐ™์€ ๊ฒฝ์šฐ๋Š” Jenkins ์ž‘์—…๊ณต๊ฐ„(workspace)์— ์žˆ๋Š” gradelw ํŒŒ์ผ์— ๋Œ€ํ•ด ์‹คํ–‰ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•ด์•ผํ•จ

 

        stage('Grant Permission') {
            steps {
                sh 'chmod +x ./backend/gradlew'
            }
        }

 

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์Šคํฌ๋ฆฝํŠธ์— Permission ํ—ˆ์šฉ์„ ํ•ด์ฃผ์—ˆ์Œ

 

4. JDK๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๋Š” ์˜ค๋ฅ˜

 

์ž๊พธ ๋นŒ๋“œํ•  ๋•Œ JDK๋ฅผ ๋ชป์ฐพ์•„์„œ ์‹คํŒจํ–ˆ์Œ 

 

์—ฌ๋Ÿฌ ๋ธ”๋กœ๊ทธ๋“ค์„ ์„คํŽด๋ณด์•˜๋Š”๋ฐ Docker๊ฐ€ ์•„๋‹ˆ๋ผ ์œ„์—์„œ ์ง„ํ–‰ํ•œ ๋ฐฉ์‹๋Œ€๋กœ Jenkins๋ฅผ ์„ค์น˜ํ•˜๊ฒŒ ๋˜๋ฉด JDK๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์„ค์ •์ด ๋˜์–ด์žˆ์ง€ ์•Š๋Š” ๊ฒƒ ๊ฐ™์Œ

=> ๋”ฐ๋กœ ์„ค์ •ํ•ด๋ณด์ž!

 

 

 

๋Œ€์‹œ๋ณด๋“œ์—์„œ Jenkins ๊ด€๋ฆฌ -> Tools ์— ๋“ค์–ด๊ฐ€ ADD JDK๋ฅผ ํ•ด์ฃผ๋ฉด๋จ


(Java 17๊ธฐ์ค€)

๋งŒ์•ฝ Java 17์ด ์„ค์น˜๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด brew install openjdk@17 ๋กœ ์„ค์น˜ํ•ด์คŒ

 

echo 'export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

 

 

์„ค์น˜๊ฐ€ ๋˜์—ˆ๋‹ค๋ฉด ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•ด์ฃผ๋ฉด๋จ

๋ช…๋ น์–ด๋ฅผ ์‚ดํŽด๋ณด๋ฉด $PATH ๋ฅผ ํ†ตํ•ด OpenJDK 17์˜ bin ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ๊ธฐ์กด PATH ์•ž์— ์ถ”๊ฐ€๋จ

 

=> ์ด๋ ‡๊ฒŒ ์„ค์ •ํ•˜๋ฉด Java ๊ด€๋ จ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•  ๋•Œ ์‹œ์Šคํ…œ์€ ๋จผ์ € OpenJDK 17์˜ bin ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ํ•ด๋‹น ๋ช…๋ น์–ด๋ฅผ ์ฐพ๊ณ , ๊ทธ ๋‹ค์Œ์— ๋‹ค๋ฅธ ๋””๋ ‰ํ† ๋ฆฌ๋“ค์„ ๊ฒ€์ƒ‰ํ•จ


(homebrew๋กœ ์„ค์น˜ํ•˜๊ฒŒ ๋˜๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ opt/homebrew/opt/openjdk@17 ์˜ ๊ฒฝ๋กœ๋กœ ์„ค์น˜๊ฐ€ ๋จ)

 

์ด์ œ 

 

Jenkins Tools ์—์„œ JAVA_HOME ๋ถ€๋ถ„์— 

/opt/homebrew/Cellar/openjdk@17/17.0.14/libexec/openjdk.jdk/Contents/Home ๋ผ๊ณ  ์„ค์น˜๋œ Java์˜ ์‹ค์ œ ๊ฒฝ๋กœ๋ฅผ ์ž…๋ ฅํ•ด์คŒ

 

(์ด๋Š” /usr/libexec/java_home -v 17๋ฅผ ํ„ฐ๋ฏธ๋„์— ์ž…๋ ฅ ํ›„ ์„ค์น˜ ๊ฒฝ๋กœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ)

 

5. docker : command not found

 

์œ„์—์„œ docker ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค์น˜ํ–ˆ๋Š”๋ฐ ์™œ ์ด๋Ÿฐ ์—๋Ÿฌ๊ฐ€ ๋‚ฌ์„๊นŒ..?

 

=> ํ”Œ๋Ÿฌ๊ทธ์ธ์€ Jenkins์™€ Docker์˜ ํ†ตํ•ฉ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€๋งŒ, ์‹ค์ œ Docker ๋ช…๋ น์–ด์˜ ์œ„์น˜๋Š” ์•Œ์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์ž„

 

ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•ด์„œ Jenkins์—๊ฒŒ Docker ์‹คํ–‰ ํŒŒ์ผ์˜ ์ •ํ™•ํ•œ ์œ„์น˜๋ฅผ ์•Œ๋ ค์ค˜์•ผํ•จ
(Jenkins๊ฐ€ sh 'docker ...' ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•  ๋•Œ, ์‹œ์Šคํ…œ์˜ PATH ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด Docker CLI ์‹คํ–‰ ํŒŒ์ผ์„ ์ฐพ์Œ)

 

environment {
    PATH = "/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:${env.PATH}"
}

 

which docker ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ /opt๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ๋‚˜์™€์„œ ์ด๋ฅผ PATH์— ์ ์œผ๋ฉด docker-credential-desktop ์ด๋ผ๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•จ

 

=> Docker Desktop์˜ ์ธ์ฆ ๋„๊ตฌ๋ฅผ ์ฐพ์ง€ ๋ชปํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜

 

Docker Desktop์˜ ๊ธฐ๋ณธ ์„ค์น˜ ๊ฒฝ๋กœ์ธ /usr/local/bin:/Applications/Docker.app/Contents/Resources/bin๋ฅผ PATH์— ์ถ”๊ฐ€ํ•˜์—ฌ docker-credential-desktop์„ ์ฐพ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์•ผํ•จ

 

ํ˜„์žฌ ๋กœ์ปฌ์—์„œ Docker Desktop์„ ์„ค์น˜ํ•˜์—ฌ ์‚ฌ์šฉ์ค‘์ด๊ธฐ์— ~/.docker/config.json ํŒŒ์ผ์˜ credsStore ์„ค์ •์ด "desktop"์œผ๋กœ ๋˜์–ด ์žˆ์Œ

=> ์ด ์„ค์ • ๋•Œ๋ฌธ์— Docker๋Š” docker-credential-desktop์„ ์ฐพ์œผ๋ ค๊ณ  ํ•˜๊ธฐ์— ์œ„์™€ ๊ฐ™์ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์ž„
(๋ฌด์กฐ๊ฑด Docker Desktop์„ ๊น”์•„์•ผํ•˜๋Š”๊ฑด ์•„๋‹˜!!)

 

๋”๋ณด๊ธฐ

์œ„์˜ PATH๋ฅผ ์„ค์ •ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋ณด๊ณ  PATH๋ฅผ ํ†ตํ•ด ์–ด๋–ป๊ฒŒ ๋ช…๋ น์–ด๊ฐ€ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ถ๊ธˆํ•ด์„œ ๋” ์ฐพ์•„๋ด„

 

์šด์˜์ฒด์ œ๋Š” PATH์— ๋‚˜์—ด๋œ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์ˆœ์ฐจ์ ์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉฐ ๋ช…๋ น์–ด์™€ ์ผ์น˜ํ•˜๋Š” ์‹คํ–‰ ํŒŒ์ผ์„ ์ฐพ์œผ๋ฉด ์ฆ‰์‹œ ๊ทธ ํŒŒ์ผ์„ ์‹คํ–‰ํ•˜๊ณ  ๊ฒ€์ƒ‰์„ ์ค‘๋‹ดํ•จ
(์™ผ์ชฝ์ด ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง€๋ฏ€๋กœ ๋™์ผํ•œ ์ด๋ฆ„์˜ ๋ช…๋ น์–ด๊ฐ€ ์—ฌ๋Ÿฌ ๋””๋ ‰ํ† ๋ฆฌ์— ์žˆ๋‹ค๋ฉด, ๊ฐ€์žฅ ์™ผ์ชฝ์— ์žˆ๋Š”๊ฒŒ ์‹คํ–‰๋จ)

 

:$PATH๋ฅผ ํ†ตํ•ด ์ด์–ด๋ถ™์ด๋Š” ์ด์œ ๋Š” ์ƒˆ๋กœ์šด ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด์„œ ๊ธฐ์กด์˜ ๋ช…๋ น์–ด๋„ ๊ณ„์† ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ์ž„
(๋งŒ์•ฝ :๋ฅผ ์“ฐ์ง€ ์•Š๊ณ  PATH = ์œผ๋กœ ๋๋ƒˆ๋‹ค๋ฉด ๊ธฐ์กด์— ์ถ”๊ฐ€ํ•œ PATH๋“ค์ด ์‚ฌ๋ผ์ง€๊ธฐ์— ๋‹ค๋ฅธ ๋ช…๋ น์–ด๋“ค์˜ ์‚ฌ์šฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•จ)

=> ๊ธฐ์กด์˜ $PATH ๊ฒฝ๋กœ ์•ž์— ์ถ”๊ฐ€

 

 

๋˜ํ•œ Jenkins ํŒŒ์ดํ”„๋ผ์ธ์„ ๋Œ๋ฆด ๋•Œ Docker ๋ฐ๋ชฌ์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด์•ผํ•จ

=> Docker Desktop์ด ์‹คํ–‰์ค‘์ด์—ฌ์•ผํ•จ

(Homebrew๋ฅผ ํ†ตํ•ด Docker Engine๋งŒ ์„ค์น˜ํ–ˆ๋‹ค๋ฉด ๋‹ค๋ฅด๊ฒŒ ์„ค์ •)

 

์ตœ์ข… ํ…Œ์ŠคํŠธ ๋ฐ ํŒŒ์ดํ”„๋ผ์ธ ์ฝ”๋“œ

 

pipeline {
    agent any
    
    environment {
        PATH = "/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:${env.PATH}"
    }

    
    stages {
        stage('Checkout') {
            steps {
                git branch: env.GITHUB_BRANCH, url: env.GITHUB_REPO_URL, credentialsId: 'ssh-key'
            }
        }
        
        stage('Inject Configuration') {
            steps {
                withCredentials([file(credentialsId: 'application.yaml', variable: 'APP_YAML')]) {
                    sh 'cp -f $APP_YAML backend/src/main/resources/application.yaml'
                }
            }
        }
        
        stage('Grant Permission') {
            steps {
                sh 'chmod +x ./backend/gradlew'
            }
        }
        
        stage('Build') {
            steps {
                dir('backend') {
                    sh "./gradlew ${env.GRADLE_TASK}"
                }
            }
        }
        
        stage('Build and Push Docker Image') {
            steps {
                sh 'docker info'
                dir('backend') {
                    withCredentials([usernamePassword(credentialsId: 'dockerhub-credentials', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
                        sh """
                            echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin
                            docker buildx build --platform ${env.DOCKER_BUILD_PLATFORMS} -t ${env.DOCKER_IMAGE_NAME} --push .
                        """
                    }
                }
            }
        }
        
        stage('Update Known Hosts') {
            steps {
                sh """
                    mkdir -p ~/.ssh
                    ssh-keyscan -H ${env.EC2_HOST} >> ~/.ssh/known_hosts
                """
            }
        }
        
        stage('Deploy to EC2') {
            steps {
                sshagent(credentials: ['ssh-key']) {
                    sh """
                        ssh ${env.EC2_USER}@${env.EC2_HOST} '
                            sudo docker stop \$(sudo docker ps -q --filter ancestor=${env.DOCKER_IMAGE_NAME})
                            sudo docker rm \$(sudo docker ps -aq --filter ancestor=${env.DOCKER_IMAGE_NAME})
                            sudo docker pull ${env.DOCKER_IMAGE_NAME}
                            sudo docker run --platform linux/amd64 -d -p ${env.CONTAINER_PORT}:${env.CONTAINER_PORT} ${env.DOCKER_IMAGE_NAME}
                        '
                    """
                }
            }
        }
    }
}

 

 

 

 

EC2์— ๋“ค์–ด๊ฐ€ sudo docker ps ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ์‹คํ–‰์ค‘์ธ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด ์ž˜ ๋™์ž‘ํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Œ

 

 

 

์ถ”๊ฐ€๋กœ ๊ฐ„๋‹จํ•œ ํšŒ์›๊ฐ€์ž… ๋กœ์ง๋„ ์ž˜ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค

 

๋ฐ”๋ผ๋ณด๊ณ  ์žˆ๋Š” Branch์˜ ์ปค๋ฐ‹ ๊ธฐ๋ก์˜ ๋ณ€ํ™”๊ฐ€ ์ƒ๊ธฐ๋ฉด ์ •ํ•ด์ง„ ์‹œ๊ฐ„๋งˆ๋‹ค ํ™•์ธ ํ›„ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋จ! 

=> ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ธธ!!
๋ฐ˜์‘ํ˜•

'๐ŸŒŠ Infra > CI ยท CD' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

Github Actions + Docker + EC2๋กœ Spring Boot CI /CD ๊ตฌ์ถ•  (0) 2025.03.23
Github Actions ํ†บ์•„๋ณด๊ธฐ  (0) 2025.02.26
GitHub Actions ์†Œ๊ฐœ  (0) 2024.05.01