목표
1. Next.js 어플리케이션 빌드 시 정적 컨텐츠를 Azure Storage Account (Blob)에 저장되도록 합니다.
2. Azure Content Delivery Network (CDN)의 원본을 Blob으로 설정하여 정적 어플리케이션의 CDN 서비스가 가능하도록 합니다.
3. 정적 컨텐츠 어플리케이션의 이미지는 Azure Container Registry(ACR)에서 관리합니다.
4. ACR의 이미지를 사용하여 Azure Kubernetes Service(AKS)의 Pod를 배포하여 CDN 서비스 테스트를 합니다.
5. 위 모든 과정(빌드, ACR 이미지 관리, AKS Pod 배포)에 대해서는 Jenkins Pipeline을 통해서 진행 되도록합니다.
6. Next.js 어플리케이션 코드는 Gitlab Repogitory에서 관리하도록 합니다.
구성
1. Application
Next.js 어플리케이션 생성 합니다.
2. Jenkins
어플리케이션 빌드 , 도커 빌드 , ACR 이미지 Push, AKS 배포 등 다양한 작업을 위한 파이프라인을 만들어 작동시킵니다.
3. Gitlab
Next.js 어플리케이션 소스코드 저장 및 관리합니다.
4. Azure Container Registry
정적 어플리케이션에 대한 Docker 이미지를 ACR에서 관리합니다.
5. Azure Kubernetes Service
ACR 이미지를 기반으로 Deployment 및 Service를 배포해서 정적 어플리케이션의 CDN 서비스가 가능하도록 합니다.
가이드
1. Azure 리소스 배포
Blob의 정적 컨텐츠를 CDN을 이용하여 서비스하기 위해서 Azure Storage Account (Blob)과 Azure CDN 배포가 필요합니다.
Azure Storage Account 배포

Azure CDN 배포

Azure CDN 엔드포인트 원본 설정을 진행합니다.
원본을 Blob으로 설정해서 정적컨텐츠를 CDN 서비스를 제공할 수 있도록 합니다.
원본 구성의 경로는 /로 설정합니다 (아무것도 입력하지 않으면 루트입니다.)

2. 로컬 테스트
Next.js 어플리케이션을 로컬에서 빌드하여 정적 컨텐츠가 어느경로에 저장이 되는지 확인합니다.
$ npm run build

위 명령어를 이용해서 빌드를 진행하면 /.next/static에 정적 컨텐츠가 저장됩니다.
.next 디렉토리는 Next.js의 기본 빌드 출력 디렉토리이며, 해당 디렉토리 하위 목록 또한 구조가 정해져있습니다.
.next/
├── static/ # 정적 파일들이 저장되는 곳
│ ├── chunks/ # 코드 청크들
│ ├── css/ # CSS 파일들
│ ├── media/ # 이미지 등 미디어 파일들
│ └── [build-id]/ # 빌드별 고유 ID를 가진 폴더

3. Gitlab 소스코드 관리
로컬의 Next.js 어플리케이션을 GitLab Repository로 업로드합니다.
(Gitlab은 Azure VM을 이용해서 구성하였으며 구성에 대한 가이드는 설명하지 않습니다.)
$ git init
$ git add .
$ git commit -m "Initial commit"
$ git remote add origin http://10.0.1.6/root/frontend-blob.git
$ git remote -v
$ git remote
$ git branch
$ git branch -M main
$ git push -f origin main

※참고
GitLab과 Jenkins는 Token 기반 연결이 완료되었습니다.
1) GitLab에서 사용자 기반 액세스 토큰을 발급하였습니다.

2) Jenkins에서 발급된 토큰 기반으로 GitLab과 연결을 완료하였습니다.
Manage Jenkins >> System >> GitLab에서 확인할 수 있습니다.

4. Jenkins Pipeline 구축
Manage Jenkins >> Tool에서 JDK, Nodejs에 대한 설정을 진행합니다.
( Jenkins 또한 Azure VM을 통해서 환경 구성하였습니다. Jenkins 서버에는 Docker, JDK, Nodejs, Azure CLI가 설치되어 있어야합니다. 환경 구성에 관한 내용은 생략합니다.)


Manage Jenkins >> Credentials에서 Gitlab과 연결을 위한 인증 정보들을 관리합니다.
GitLab과 연결하기 위해서 "Gitlab_jenkins_push"을 사용하였고 실제 파이프라인 구축시 인증을 위해서는 "gitlab_pw"를 사용하였습니다.

Manage Jenkins >> Plugins에서 파이프라인을 실행하면서 필요한 Plugins을 설치합니다.

새 파이프라인을 만들고 파이프라인의 Configure로 들어갑니다.
제일 아래 Pipeline 스크립트를 작성합니다.
다음은 Pipeline 스크립트에 대한 설명입니다.
1) Cleanup workspace: 작업 시작 전 작업 공간 클리어합니다.
2) Checkout : GitLab에서 소스코드를 가져오며, 'gitlab_pw' 인증을 사용하여 인증, gitlab의 main 브랜치 코드를 체크아웃합니다. 여기서 체크아웃은 Gitlab에서 코드를 Jenkins 서버로 가져오는것을 말합니다.
3) Install Dependencies: npm ci 명령어로 의존성을 설치합니다. Javascript의 package-lock.json을 따라 설치를 진행합니다.
4) Build : Next.js 애플리케이션 빌드 수행하며 .next 디렉퇴에 빌드 결과물을 생성합니다.
5) Upload to Azure Storage : Azure Storage에 정적 파일을 업로드하고 기존에 있던 정적 파일이 있으면 삭제를 진행합니다. 그리고 CDN 캐시를 퍼지를 진행하여 깨끗한 환경을 구성합니다.
6) Docker Build & Push : ACR 로그인을 진행하고 Docker 이미지를 빌드 및 푸시합니다. 최신 태그와 빌드 번호 태그를 생성하여 ACR로 Docker 이미지를 푸쉬합니다.
7) Deploy to AKS : Manifest 파일 기바으로 AKS에 어플리케이션을 배포합니다.
※ 참고
1) agent any : 파이프라인을 실행할 수 있는 Jenkins 에이전트를 지정합니다. 여기서는 아무 에이전트나 사용할 수 있음을 의미합니다.
2) tool { nodejs 'nodejs' } : 파이프라인에서 사용할 도구를 지정합니다. 여기서는 nodejs라는 이름의 Nodejs를 사용합니다.
pipeline {
agent any
tools {
nodejs 'nodejs'
}
environment {
ACR_URL = 'terzmyacr.azurecr.io'
IMAGE_NAME = 'front-app1'
IMAGE_TAG = "${BUILD_NUMBER}"
SERVICE = 'frontend'
AZURE_STORAGE_CONTAINER = 'webapp'
AZURE_STORAGE_ACCOUNT = 'testlscstg'
STATIC_BUILD_PATH = './.next/static'
AZURE_RESOURCE_GROUP = 'cheol-rg'
AZURE_CDN_PROFILE = 'terzmy-cdn'
AZURE_CDN_ENDPOINT = 'terzmy-blob'
MANIFEST_PATH = './manifest'
USER_ASSIGNED="93aadcb7-0119-44e7-ad16-ba1e7e029333"
NEXT_PUBLIC_DEPLOY_MODE='develop'
NEXT_PUBLIC_CDN_URL='https://terzmy-blob.azureedge.net'
}
stages {
stage('Cleanup Workspace') {
steps {
cleanWs()
}
}
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://10.0.1.6/root/frontend-blob.git',
credentialsId: 'gitlab_pw'
}
}
stage('Install Dependencies') {
steps {
sh 'npm ci'
}
}
stage('Build') {
steps {
sh '''
npm run build
'''
}
}
stage('Upload to Azure Storage') {
steps {
script {
echo "============================================="
echo "Uploading static content to Azure Storage"
echo "============================================="
sh 'az login --identity -u ${USER_ASSIGNED}'
sh """
# Upload the entire .next directory
az storage blob upload-batch \
--destination ${AZURE_STORAGE_CONTAINER}/_next \
--source .next \
--account-name ${AZURE_STORAGE_ACCOUNT} \
--auth-mode login \
--overwrite true \
--pattern "*"
az cdn endpoint purge \
--resource-group ${AZURE_RESOURCE_GROUP} \
--profile-name ${AZURE_CDN_PROFILE} \
--name ${AZURE_CDN_ENDPOINT} \
--content-paths "/*"
"""
echo """
=============================================
✅ Static content uploaded successfully!
Storage Account: ${AZURE_STORAGE_ACCOUNT}
Container: ${AZURE_STORAGE_CONTAINER}
CDN Endpoint: ${AZURE_CDN_ENDPOINT}
=============================================
"""
}
}
}
stage('Docker Build & Push') {
steps {
script {
// Azure login using managed identity
sh 'az acr login --name $(echo ${ACR_URL} | cut -d "." -f1)'
// Build Docker image with build args
sh """
docker build \
--build-arg NEXT_PUBLIC_DEPLOY_MODE=${NEXT_PUBLIC_DEPLOY_MODE} \
-t ${ACR_URL}/${IMAGE_NAME}:${IMAGE_TAG} \
-t ${ACR_URL}/${IMAGE_NAME}:latest \
.
"""
// Push images to ACR
sh """
docker push ${ACR_URL}/${IMAGE_NAME}:${IMAGE_TAG}
docker push ${ACR_URL}/${IMAGE_NAME}:latest
"""
}
}
}
stage('Deploy to AKS') {
steps {
script {
echo "============================================="
echo "Deploying to Kubernetes using manifests"
echo "============================================="
// manifest 파일의 이미지 태그 업데이트
sh """
sed -i 's|image: ${ACR_URL}/${IMAGE_NAME}:.*|image: ${ACR_URL}/${IMAGE_NAME}:${IMAGE_TAG}|' ${MANIFEST_PATH}/deployment.yaml
"""
// Kubernetes에 배포
sh """
kubectl apply -f ${MANIFEST_PATH}/deployment.yaml -n ${SERVICE}
kubectl apply -f ${MANIFEST_PATH}/service.yaml -n ${SERVICE}
kubectl rollout status deployment/${IMAGE_NAME} -n ${SERVICE}
"""
echo """
=============================================
✅ Deployment completed successfully!
Namespace: ${SERVICE}
Image: ${ACR_URL}/${IMAGE_NAME}:${IMAGE_TAG}
=============================================
"""
}
}
}
}
post {
success {
sh "docker rmi ${ACR_URL}/${IMAGE_NAME}:${IMAGE_TAG}"
sh "docker rmi ${ACR_URL}/${IMAGE_NAME}:latest"
}
failure {
echo """
=============================================
❌ Pipeline failed!
Workspace preserved for debugging.
Path: ${WORKSPACE}
=============================================
"""
}
}
}
Blob 스토리지의 webapp이라는 컨테이너에 _next/static 경로에 정적컨텐츠를 업로드합니다.
# Upload the entire .next directory
az storage blob upload-batch \
--destination ${AZURE_STORAGE_CONTAINER}/_next \
--source .next \
--account-name ${AZURE_STORAGE_ACCOUNT} \
--auth-mode login \
--overwrite true \
--pattern "*"
위 Jenkins 파이프라인 스크립트를 실행합니다.
4. 결과
Jenkins 파이프라인을 통해서 Next.js 어플리케이션의 정적 컨텐츠가 정상적으로 Blob에 업로드 되었는지 확인합니다.
정확하게 webapp/_next/static에 업로드 되었는지 확인합니다.
( '{컨테이너이름}/{폴더명}/{폴더명}/정적컨텐츠'로 생각하시면 됩니다.)
※ 참고
blob에서의 폴더는 저희가 흔히 알고있는 폴더가 아닌 파일 경로를 시각적으로 표현해주는 역할을 하는 것 입니다.
아래 이미지와 같이 정적 컨텐츠에 대한 정보가 업로드 되어 있습니다.
1) chunks : 어플리케이션 코드를 작은 단위로 분할한 JavaScript 파일들이 저장됩니다.
2) css : 애플리케이션의 모든 CSS 파일들을 저장합니다.
3) media : 이미지, 폰트 등 미디어 파일들이 저장됩니다.
4) 랜덤 이름 폴더 : 빌드마다 생성되는 고유한 해시값을 가진 폴더입니다. 해당 폴더에는 _buildManifest.js와 _ssgManifest.js 폴더가 있습니다.
5) version.json : 현재 빌드의 버전 정보를 담고 있는 파일입니다.

※ 참고
1) _buildManifest.js
애플리케이션의 모든 페이지와 관련된 JavaScript bundle/chunks 매핑 정보를 포함합니다.
클라이언트 사이드 라우팅에 필요한 페이지별 의존성 정보를 포함합니다.
Next.js가 페이지 전환시 어떤 Javascript를 미리 로드해야하는지 결정합니다.
! 클라이언트 사이드 라우팅은 처음에 필요한 코드를 모두 받아온 후, 페이지 이동시 필요한 부분만 변경하는 것입니다
예를들면 인스타그램에서 게시물을 옆으로 넘길때 필요한 부분만 변경되는것을 말합니다.
2) _ssgManifest.js
ssg(Static Site Generation)와 관련된 정보가 포함되어 있습니다.
정적 페이지와 동적 페이지 구분 정보를 제공한다.
ssg(정적 사이트 생성)은 빌드 시 리액트 앱을 HTML로 미리 랜더링합니다.

젠킨스 파이프라인을 통해 배포된 Pod에 접속해서 curl 테스트를 진행합니다.
$ kubectl exec -it -n frontend front-app1-6b745bc58b-qcmdf -- sh
/app # curl http://localhost:3000

웹 브라우저로 접근 테스트를 하기 위해서 Service type을 LoadBalancer로 설정하고 테스트를 진행해봅니다.

웹브라우저 개발자 도구를 통해서 어디서 정적 컨텐츠를 가져오는지 확인합니다.
개발자도구 Network에서 Request URL을 볼 수 있습니다. 해당 URL 경로를 봤을때 두번째 이미지에서 Blob의 동일한 경로에서 JavaScript를 가져오는 것 을 알수있습니다. 해당 Blob 스토리지는 CDN의 원본으로 사용되고 있으며 정적 컨텐츠가 CDN으로 정상적으로 서비스 되는 것을 알 수 있습니다.


'Azure' 카테고리의 다른 글
| Azure Kubernetes Service 가용성 & Taint (0) | 2025.01.31 |
|---|---|
| WebSocket 통신과 Azure Front Door (0) | 2025.01.15 |
| Azure Kubernetes Service & Grafana Tempo (0) | 2024.11.01 |
| Azure Application Insights를 이용한 Application Monitoring (nodejs)- 2/2 (1) | 2024.10.31 |
| Azure Application Insights를 이용한 Application Monitoring (nodejs)- 1/2 (0) | 2024.10.31 |