
DEV ๋ฐฐํฌ์ฉ workflow ์์ฑ
PR ํ ์คํธ์ฉ์ ์ด์ด์, feature ๋ธ๋์น๋ค์ด dev ๋ธ๋์น์ merge๋์ด push๋ ๋ ๋์ ํ workflow๋ฅผ ์์ ํด๋ณด์๋ค.
Github Actions๋ฅผ ํตํด CI/CD ํด๋ณธ ๊ฒ ์ฒ์์ด๋ผ ํท๊ฐ๋ฆฌ๊ณ ์ดํด ์ ๋๋ ๋ด์ฉ์ด ๋ง์์, ๋ด์ฉ ๊ธฐ๋กํ๋ฉด์ ์ ๋ฆฌํด๋ณด์๋ค.
Spring Boot ๋น๋
- name: Build with Gradle (DEV) - Skip Tests
run: ./gradlew clean build -x test --no-daemon
env:
SPRING_PROFILES_ACTIVE: dev
Gradle์ ์ฌ์ฉํ์ฌ ์คํ๋ง ๋ถํธ ํ๋ก์ ํธ๋ฅผ ๋น๋ํ๋ค. -x test ๋ช ๋ น์ด๋ก ํ ์คํธ๋ ์ ์ธ์์ผฐ๋ค. (ํ ์คํธ๋ Pull Request ํ ๋ ์งํ)
๋น๋๊ฐ ์ฑ๊ณตํ๋ฉด build/libs/ ํด๋ ์์ ์คํ ๊ฐ๋ฅํ JAR ํ์ผ์ด ์์ฑ๋๋ค.
Docker ์ด๋ฏธ์ง ๋น๋ - App
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build App Docker image (DEV)
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: false
tags: |
${{ env.APP_IMAGE_NAME }}:${{ github.sha }}
${{ env.APP_IMAGE_NAME }}:latest-dev
build-args: |
SPRING_PROFILES_ACTIVE=dev
cache-from: type=gha
cache-to: type=gha,mode=max
load: true
์คํ๋ง ๋ถํธ ํ๋ก์ ํธ ๋ด ./Dockerfile์ ์ฝ์ด์ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.
cache-from: type=gha
โก๏ธ ๋น๋๋ฅผ ์์ํ๊ธฐ ์ ์, GitHub Actions ์ ์ฉ ์บ์ ์ ์ฅ์(type=gha)์์ ์ด์ ์ ๋น๋ํ๋ ๊ธฐ๋ก์ ์ฐพ์๋ณด๋ ์ค์
cache-to: type=gha,mode=max
โก๏ธ ์ด๋ฒ์ ๋น๋ํ ๊ฒฐ๊ณผ๋ฌผ์ ๋ค์๋ฒ ๋น๋ ๋ ์ธ ์ ์๋๋ก GitHub Actions ์บ์ ์ ์ฅ์์ ์ ์ฅํ๋ ์ค์
โก๏ธ max ๊ฐ์ ์ฌ์ฉํด์ผ, ๋น๋ ์ค๊ฐ์ค๊ฐ์ ์์ฑ๋ ๋ชจ๋ ์ค๊ฐ ๋จ๊ณ(Intermediate Layers)๊น์ง ์ ์ฅ๋๊ณ , Gradle ์์กด์ฑ ๋ค์ด๋ก๋ ๋ด์ญ ๊ฐ์ ์ค๊ฐ ๊ณผ์ ๊น์ง ์บ์๋์ด, ๋ค์ ๋น๋ ๋ ๋ค์ด๋ก๋๋ฅผ ๋ ์ ํ๊ณ ๋์ด๊ฐ ์ ์๋ค.
# ./Dockerfile
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY build/libs/*.jar app.jar
ENV TZ=Asia/Seoul
ENTRYPOINT ["java", "-jar", "app.jar"]
Dockerfile์ ๋น๋ ๊ด๋ จ ๋ช ๋ น์ด๋ ์๊ณ ,
Springboot ๋น๋ ๋จ๊ณ์์ ๋ง๋ค์ด๋จ๋ JAR ํ์ผ์ ๋ณต์ฌํด์ ์คํ์ ๋ด๋นํ๊ฒ ๋๋ค.
Docker ์ด๋ฏธ์ง ๋น๋ - Nginx
- name: Build Nginx Docker image (DEV)
uses: docker/build-push-action@v5
with:
context: .
file: ./nginx/Dockerfile
push: false
tags: |
${{ env.NGINX_IMAGE_NAME }}:${{ github.sha }}
${{ env.NGINX_IMAGE_NAME }}:latest-dev
build-args: |
ENVIRONMENT=dev
cache-from: type=gha
cache-to: type=gha,mode=max
load: true
ํ๋ก์ ํธ ๋ด ./nginx/Dockerfile ์ ์ฝ์ด์ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.
๋๋จธ์ง๋ APP๋น๋์ ๋์ผํ ๋ฐฉ์์ด๋ค.
AWS Lightsail ๋ฐฐํฌ ์ค๋น ๋ฐ ์ด๋ฏธ์ง ํธ์
- 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: Install Lightsail Plugin
run: |
curl "https://s3.us-west-2.amazonaws.com/lightsailctl/latest/linux-amd64/lightsailctl" -o "/usr/local/bin/lightsailctl"
sudo chmod +x /usr/local/bin/lightsailctl
- name: Push App image to Lightsail
run: |
aws lightsail push-container-image \
--service-name ${{ env.SERVICE_NAME }} \
--label app-${{ github.sha }} \
--image ${{ env.APP_IMAGE_NAME }}:${{ github.sha }} \
--region ${{ env.AWS_REGION }}
- name: Get pushed image names
id: image-names
run: |
APP_IMAGE=$(aws lightsail get-container-images --service-name ${{ env.SERVICE_NAME }} --region ${{ env.AWS_REGION }} | jq -r '.containerImages[] | select(.image | contains("app-${{ github.sha }}")) | .image' | head -n 1)
NGINX_IMAGE=$(aws lightsail get-container-images --service-name ${{ env.SERVICE_NAME }} --region ${{ env.AWS_REGION }} | jq -r '.containerImages[] | select(.image | contains("nginx-${{ github.sha }}")) | .image' | head -n 1)
if [[ -z "$APP_IMAGE" ]]; then
echo "Error: Could not find APP_IMAGE matching ${{ github.sha }}"
exit 1
fi
echo "app_image=$APP_IMAGE" >> $GITHUB_OUTPUT
echo "nginx_image=$NGINX_IMAGE" >> $GITHUB_OUTPUT
echo "Found App Image: $APP_IMAGE"
echo "Found Nginx Image: $NGINX_IMAGE"
GitHub Secrets์ ์ ์ฅ๋ AWS ํค๋ก ๋ก๊ทธ์ธ์ ํด์ฃผ๊ณ , Lightsail ์ปจํ ์ด๋ ์๋น์ค ์ด์ฉํ๊ธฐ ์ํ ํ๋ฌ๊ทธ์ธ์ ์ค์นํด์ค๋ค.
์ ๋จ๊ณ์์ ๋น๋ํ Docker ์ด๋ฏธ์ง๋ฅผ AWS Lightsail์ ์์ฒด ๋ ์ง์คํธ๋ฆฌ๋ก ์ ๋ก๋ํ๋ค.
๋ง์ง๋ง์ผ๋ก ๋ฐฉ๊ธ AWS Lightsail ๋ ์ง์คํธ๋ฆฌ์ ์ฌ๋ ค์ง ์ด๋ฏธ์ง๋ฅผ ์ฐพ๋๋ค.
aws lightsail push-container-image ๋ช ๋ น์ด๋ก ์ด๋ฏธ์ง๋ฅผ ์ฌ๋ฆฌ๋ฉด, Lightsail ์ ์ฉ ๋ ์ง์คํธ๋ฆฌ ์ฃผ์๋ก ์ด๋ฆ์ ๋ฐ๊ปด์ ์ ์ฅ๋๊ธฐ ๋๋ฌธ์ ๋ค์ Get ํด์ค๋ ๊ณผ์ ์ด ํ์ํ๋ค. ์ฐพ์ ์ด๋ฏธ์ง๋ ๋ฐฐํฌ json ํ์ผ์ ์ฌ์ฉ๋๋ค.
๋ฐฐํฌ ์ค์ json ์์ฑ
์์ฒญ ์ ๋จน๊ณ ์ ์ผ ๋ง์ด ์์ ํ step์ด๋ค.. ๐คฏ
jq๋ก JSON ๋ค๋ฃจ๊ธฐ
์ฒ์์๋ cat ๋ช ๋ น์ด๋ฅผ ์ด์ฉํ์ฌ ํ ์คํธ๋ฅผ ํ์ผ๋ก ์ ์ฅํ๋ ๋จ์ํ ๋ฐฉ์์ ์ฌ์ฉํ๋๋ฐ,
์๋์ฒ๋ผ ๋ฌธ๋ฒ์ ์ค๋ฅ๊ฐ ์์ด๋ json ํ์ผ์ด ๋ง๋ค์ด์ง๊ณ ๋ฐฐํฌ ๋๋ ๊ณผ์ ์์์ผ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.

์ด๋ฌํ ์ํฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด์ jq๋ฅผ ์ฌ์ฉํด๋ณด์๋ค.
- name: Create deployment configuration
run: |
jq -n \
--arg db_url "${{ secrets.DEV_DB_URL }}" \
--arg db_user "${{ secrets.DEV_DB_USERNAME }}" \
--arg db_pass "${{ secrets.DEV_DB_PASSWORD }}" \
--arg redis_pass "${{ secrets.DEV_REDIS_PASSWORD }}" \
# .. ์๋ต
'{
containers: {
app: {
image: $app_image,
environment: {
SPRING_PROFILES_ACTIVE: "dev",
DEV_DB_URL: $db_url,
# .. ์๋ต
},
# .. ์๋ต
}
}' > deployment.json
echo "=== Deployment Configuration ==="
cat deployment.json
jq ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฌธ๋ฒ์ ์ธ ์ค๋ฅ๊ฐ ์๋ค๋ฉด ํ์ผ์ ๋ง๋ค๊ธฐ ์ ์ ์๋ฌ๋ฅผ ๋ฑ๋๋ค. ์ฆ, ์๋ชป๋ JSON ํ์ผ์ด ์์ฑ๋๋ ๊ฒ์ ๋ง์ ์ ์๋ค.
๋ํ, --arg๋ก ๋ฐ์ ๊ฐ์ ์๋์ผ๋ก JSON ๋ฌธ์์ด ๊ท๊ฒฉ์ ๋ง๊ฒ ๋ณํํด์ฃผ๋ฏ๋ก ํ๊ฒฝ ๋ณ์๋ฅผ ๋ค๋ฃจ๊ธฐ์ ์ ํฉํ๋ค.
Lightsail ์ปจํ ์ด๋์ ๋ฐฐํฌ
- name: Deploy to Lightsail
run: |
aws lightsail create-container-service-deployment \
--service-name ${{ env.SERVICE_NAME }} \
--region ${{ env.AWS_REGION }} \
--cli-input-json file://deployment.json
์์์ ๋ง๋ค์ด์ง deployment.json์ Lightsail์ ์ ์กํ์ฌ ๋ฐฐํฌํ๋ ๋จ๊ณ์ด๋ค.
redis ์ปจํ ์ด๋ ์ฐ๊ฒฐํ๊ธฐ

๋ฐฐํฌ๋ ํ์ง๋ง Health check ์คํจ โ ๏ธ.
App ์ปจํ ์ด๋ ๋ก๊ทธ๋ฅผ ์ดํด๋ณด๋ RedisConnectionException์ด ๋ฐ์ํ๊ณ ์์๋ค.
AWS lightsail์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ง์์ ํ๊ณ ์์ด์ DB ์ฐ๊ฒฐ์ ์์ฝ๊ฒ ์งํํ๋๋ฐ, Redis ์ฐ๊ฒฐํ๋ ๊ฑด ์ ๋ง ๋๋ฌด! ํ๋ค์๋ค.
์ ๋ฏธ๋์ด์๊ฒ ๋์์ ์ป์ ๊ฒฐ๊ณผ,
AWS Lightsail์์๋ ์ปจํ ์ด๋๋ผ๋ฆฌ localhost๋ก ํต์ ํ๊ธฐ ๋๋ฌธ์ host ๊ฐ์ localhost๋ก ์ค์ ํด์ผ ํ๋ค.
App ์ปจํ ์ด๋ ์ ์ฅ์์๋ redis๋ผ๋ ์ด๋ฆ์ ๋ณ๋ ์๋ฒ๊ฐ ์๋๋ผ, ์๊ธฐ ์์ ์ localhost์ ๋ ์๋ ์๋น์ค๋ก ์ธ์ํ๋ค๊ณ ํ๋ค.
๋ํ, Redis๋ ๊ธฐ๋ณธ์ ์ผ๋ก IPv4 ์ฃผ์์ธ 127.0.0.1๋ง ๋ฆฌ์ค๋ ํ๊ณ ์๊ธฐ ๋๋ฌธ์ IPv6 ์ฃผ์๋ก ์ ํธ๋ฅผ ๋ณด๋ด๋ App ์ปจํ ์ด๋๊ฐ ์ ๊ทผํ์ง ๋ชป ํ๋ ๊ฒฝ์ฐ๊ฐ ์๋ค๊ณ ํ๋ค. ๊ทธ๋์ --bind 0.0.0.0์ ์ค์ ํ์ฌ ๋ชจ๋ ๊ณณ์์ ์ ์์ด ํ์ฉ๋๋๋ก ํด์ผํ๋ค.
Redis ํฌํธ(6379)๋ publicEndpoint๋ก ์ค์ ํ์ง ์์๊ธฐ ๋๋ฌธ์ ์ธ๋ถ์์๋ ์ ๊ทผ์ด ๋ถ๊ฐ๋ฅํ๋ฉฐ, ๊ฐ์ Lightsail ์ปจํ ์ด๋ ์๋น์ค ๋ด๋ถ์์๋ง ์ ์์ด ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์ ๋ณด์ ์ด์๋ ์๋ค.
# deployment.json
redis: {
image: "redis:7-alpine",
command: ["redis-server", "--requirepass", $redis_pass],
ports: {
"6379": "TCP"
}
}
local , test ํ๊ฒฝ์์๋ ๋น๋ฐ๋ฒํธ ์์ด redis์ ์ ์ํ๋ค ๋ณด๋ ์ฐ๊ฒฐ์ด ์ ๋์๋๋ฐ
dev์๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ ์์ด ํ์ํ์ฌ redis conifg ์ค์ ๋ ์์ ํด์คฌ๋ค.
@Configuration
class RedisConfig(
@Value("\${spring.data.redis.host}") private val host: String,
@Value("\${spring.data.redis.port}") private val port: Int,
@Value("\${spring.data.redis.password}") private val password: String,
) {
@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
val config = RedisStandaloneConfiguration(host, port)
config.setPassword(password)
return LettuceConnectionFactory(config)
}
@Bean
fun stringRedisTemplate(connectionFactory: RedisConnectionFactory): StringRedisTemplate =
StringRedisTemplate(connectionFactory)
}

redis ์ฐ๊ฒฐ์ ์ฑ๊ณตํด์ Health check๋ ํต๊ณผ๋๋ค.

์ค๋๋ ์ฝ์ง ๋์ ๋ฐฐํฌ ์ฑ๊ณต .. โ๐ป
์ข๋ ์ธํ๋ผ ์ง์์ ์์์ ๋์น ๋ถ๋ถ์ ๊ฐ์ ํด ๋๊ฐ์ผ๊ฒ ๋ค.