mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2026-06-13 12:09:10 +00:00
Jenkins CI/CD 파이프라인 완전 구축 및 가이드 개선
주요 작업: - Kustomize 기반 환경별 배포 구조 완성 (dev/staging/prod) - deployment-patch.yaml 개선: replicas + resources 통합 관리 - Strategic Merge Patch 형식으로 변경하여 가독성 및 유지보수성 향상 - 환경별 차등 리소스 할당 정책 적용 - Jenkins 파이프라인 스크립트 및 수동 배포 스크립트 완성 - 상세한 체크리스트 및 실수 방지 가이드 추가 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Regular → Executable
+65
-457
@@ -1,474 +1,82 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Manual Deployment Script for phonebill Microservices
|
||||
# Usage: ./deploy.sh <environment> [service1,service2,...] [options]
|
||||
# Example: ./deploy.sh dev all --skip-build
|
||||
# Example: ./deploy.sh prod user-service,bill-service --force
|
||||
ENVIRONMENT=${1:-dev}
|
||||
IMAGE_TAG=${2:-latest}
|
||||
|
||||
set -euo pipefail
|
||||
echo "🚀 Starting deployment to ${ENVIRONMENT} environment with image tag: ${IMAGE_TAG}"
|
||||
|
||||
# Script configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
CICD_DIR="${PROJECT_ROOT}/deployment/cicd"
|
||||
K8S_DIR="${PROJECT_ROOT}/deployment/k8s"
|
||||
# 환경 검증
|
||||
if [[ ! "$ENVIRONMENT" =~ ^(dev|staging|prod)$ ]]; then
|
||||
echo "❌ Error: Invalid environment. Use dev, staging, or prod"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
# Kustomize 설치 확인
|
||||
if ! command -v kustomize &> /dev/null; then
|
||||
echo "📦 Installing Kustomize..."
|
||||
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
|
||||
sudo mv kustomize /usr/local/bin/
|
||||
fi
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
# kubectl 연결 확인
|
||||
if ! kubectl cluster-info &> /dev/null; then
|
||||
echo "❌ Error: Unable to connect to Kubernetes cluster"
|
||||
echo "Please ensure kubectl is configured properly"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
# 네임스페이스 생성
|
||||
echo "🔧 Creating namespace phonebill-${ENVIRONMENT} if not exists..."
|
||||
kubectl create namespace phonebill-${ENVIRONMENT} --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
# 환경별 디렉토리로 이동
|
||||
cd deployment/cicd/kustomize/overlays/${ENVIRONMENT}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
echo "🏷️ Updating image tags..."
|
||||
|
||||
# Usage information
|
||||
show_usage() {
|
||||
cat << EOF
|
||||
Usage: $0 <environment> [services] [options]
|
||||
# 각 서비스 이미지 태그 업데이트
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/api-gateway:${ENVIRONMENT}-${IMAGE_TAG}
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/user-service:${ENVIRONMENT}-${IMAGE_TAG}
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/bill-service:${ENVIRONMENT}-${IMAGE_TAG}
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/product-service:${ENVIRONMENT}-${IMAGE_TAG}
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/kos-mock:${ENVIRONMENT}-${IMAGE_TAG}
|
||||
|
||||
Arguments:
|
||||
environment Target environment (dev|staging|prod)
|
||||
services Services to deploy (default: all)
|
||||
Options: all, api-gateway, user-service, bill-service, product-service, kos-mock
|
||||
Multiple services: service1,service2,service3
|
||||
echo "📋 Applying Kubernetes manifests..."
|
||||
|
||||
Options:
|
||||
--skip-build Skip Gradle build step
|
||||
--skip-test Skip unit tests during build
|
||||
--skip-push Skip container image push
|
||||
--force Force deployment even if no changes
|
||||
--dry-run Show what would be deployed without actually deploying
|
||||
--help Show this help message
|
||||
# 배포 실행
|
||||
kubectl apply -k .
|
||||
|
||||
Examples:
|
||||
$0 dev # Deploy all services to dev
|
||||
$0 staging user-service # Deploy user-service to staging
|
||||
$0 prod api-gateway,bill-service # Deploy specific services to prod
|
||||
$0 dev all --skip-build # Deploy without building
|
||||
$0 staging all --dry-run # Preview deployment
|
||||
echo "⏳ Waiting for deployments to be ready..."
|
||||
|
||||
Environment Files:
|
||||
dev: ${CICD_DIR}/config/deploy_env_vars_dev
|
||||
staging: ${CICD_DIR}/config/deploy_env_vars_staging
|
||||
prod: ${CICD_DIR}/config/deploy_env_vars_prod
|
||||
EOF
|
||||
}
|
||||
# 배포 상태 확인
|
||||
kubectl rollout status deployment/${ENVIRONMENT}-api-gateway -n phonebill-${ENVIRONMENT} --timeout=300s
|
||||
kubectl rollout status deployment/${ENVIRONMENT}-user-service -n phonebill-${ENVIRONMENT} --timeout=300s
|
||||
kubectl rollout status deployment/${ENVIRONMENT}-bill-service -n phonebill-${ENVIRONMENT} --timeout=300s
|
||||
kubectl rollout status deployment/${ENVIRONMENT}-product-service -n phonebill-${ENVIRONMENT} --timeout=300s
|
||||
kubectl rollout status deployment/${ENVIRONMENT}-kos-mock -n phonebill-${ENVIRONMENT} --timeout=300s
|
||||
|
||||
# Parse command line arguments
|
||||
parse_arguments() {
|
||||
if [[ $# -eq 0 ]] || [[ "$1" == "--help" ]]; then
|
||||
show_usage
|
||||
exit 0
|
||||
fi
|
||||
echo "🔍 Health Check..."
|
||||
|
||||
ENVIRONMENT="$1"
|
||||
shift
|
||||
# API Gateway Health Check
|
||||
GATEWAY_POD=$(kubectl get pod -n phonebill-${ENVIRONMENT} -l app=api-gateway -o jsonpath='{.items[0].metadata.name}')
|
||||
if kubectl -n phonebill-${ENVIRONMENT} exec $GATEWAY_POD -- curl -f http://localhost:8080/actuator/health > /dev/null 2>&1; then
|
||||
echo "✅ API Gateway is healthy!"
|
||||
else
|
||||
echo "⚠️ API Gateway health check failed, but deployment completed"
|
||||
fi
|
||||
|
||||
# Validate environment
|
||||
if [[ ! "$ENVIRONMENT" =~ ^(dev|staging|prod)$ ]]; then
|
||||
log_error "Invalid environment: $ENVIRONMENT"
|
||||
log_error "Valid environments: dev, staging, prod"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set services (default to 'all')
|
||||
SERVICES_TO_DEPLOY="all"
|
||||
if [[ $# -gt 0 ]] && [[ ! "$1" =~ ^-- ]]; then
|
||||
SERVICES_TO_DEPLOY="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# Parse options
|
||||
SKIP_BUILD=false
|
||||
SKIP_TEST=false
|
||||
SKIP_PUSH=false
|
||||
FORCE_DEPLOY=false
|
||||
DRY_RUN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--skip-build)
|
||||
SKIP_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--skip-test)
|
||||
SKIP_TEST=true
|
||||
shift
|
||||
;;
|
||||
--skip-push)
|
||||
SKIP_PUSH=true
|
||||
shift
|
||||
;;
|
||||
--force)
|
||||
FORCE_DEPLOY=true
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Load environment configuration
|
||||
load_environment_config() {
|
||||
local config_file="${CICD_DIR}/config/deploy_env_vars_${ENVIRONMENT}"
|
||||
|
||||
if [[ ! -f "$config_file" ]]; then
|
||||
log_error "Configuration file not found: $config_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Source the configuration file
|
||||
set -a # automatically export all variables
|
||||
source "$config_file"
|
||||
set +a
|
||||
|
||||
log_info "Loaded configuration from $config_file"
|
||||
}
|
||||
|
||||
# Validate prerequisites
|
||||
validate_prerequisites() {
|
||||
local missing_tools=()
|
||||
|
||||
# Check required tools
|
||||
command -v gradle >/dev/null 2>&1 || missing_tools+=("gradle")
|
||||
command -v docker >/dev/null 2>&1 || missing_tools+=("docker")
|
||||
command -v kubectl >/dev/null 2>&1 || missing_tools+=("kubectl")
|
||||
command -v az >/dev/null 2>&1 || missing_tools+=("azure-cli")
|
||||
|
||||
if [[ ${#missing_tools[@]} -ne 0 ]]; then
|
||||
log_error "Missing required tools: ${missing_tools[*]}"
|
||||
log_error "Please install the missing tools and try again"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we're in the project root
|
||||
if [[ ! -f "${PROJECT_ROOT}/settings.gradle" ]]; then
|
||||
log_error "Not in phonebill project root directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Azure login
|
||||
if ! az account show >/dev/null 2>&1; then
|
||||
log_error "Not logged into Azure CLI. Please run: az login"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check kubectl context
|
||||
if ! kubectl config current-context >/dev/null 2>&1; then
|
||||
log_warn "No kubectl context set. Will attempt to configure AKS credentials"
|
||||
fi
|
||||
|
||||
log_success "Prerequisites validation passed"
|
||||
}
|
||||
|
||||
# Resolve services list
|
||||
resolve_services() {
|
||||
if [[ "$SERVICES_TO_DEPLOY" == "all" ]]; then
|
||||
SERVICE_LIST=(${SERVICES//,/ })
|
||||
else
|
||||
IFS=',' read -ra SERVICE_LIST <<< "$SERVICES_TO_DEPLOY"
|
||||
fi
|
||||
|
||||
log_info "Services to deploy: ${SERVICE_LIST[*]}"
|
||||
|
||||
# Validate service names
|
||||
local valid_services=(${SERVICES//,/ })
|
||||
for service in "${SERVICE_LIST[@]}"; do
|
||||
if [[ ! " ${valid_services[*]} " =~ " ${service} " ]]; then
|
||||
log_error "Invalid service name: $service"
|
||||
log_error "Valid services: ${valid_services[*]}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Build services
|
||||
build_services() {
|
||||
if [[ "$SKIP_BUILD" == true ]]; then
|
||||
log_info "Skipping build step"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Building services with Gradle..."
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
for service in "${SERVICE_LIST[@]}"; do
|
||||
log_info "Building $service..."
|
||||
|
||||
local build_cmd="./gradlew ${service}:clean ${service}:build --no-daemon --parallel"
|
||||
|
||||
if [[ "$SKIP_TEST" == true ]]; then
|
||||
build_cmd="$build_cmd -x test"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would execute: $build_cmd"
|
||||
else
|
||||
if ! $build_cmd; then
|
||||
log_error "Build failed for $service"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Build completed successfully"
|
||||
}
|
||||
|
||||
# Build and push container images
|
||||
build_and_push_images() {
|
||||
if [[ "$SKIP_PUSH" == true ]]; then
|
||||
log_info "Skipping container image build and push"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Building and pushing container images..."
|
||||
|
||||
# Generate image tag
|
||||
local timestamp=$(date +%Y%m%d-%H%M%S)
|
||||
local build_number="${BUILD_NUMBER:-$(date +%s)}"
|
||||
IMAGE_TAG="${build_number}-${ENVIRONMENT}-${timestamp}"
|
||||
|
||||
log_info "Using image tag: $IMAGE_TAG"
|
||||
|
||||
# Login to ACR
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
log_info "Logging into Azure Container Registry..."
|
||||
az acr login --name "$ACR_NAME"
|
||||
fi
|
||||
|
||||
for service in "${SERVICE_LIST[@]}"; do
|
||||
log_info "Building container image for $service..."
|
||||
|
||||
local image_name="${REGISTRY_URL}/phonebill/${service}"
|
||||
local service_dir="${PROJECT_ROOT}/${service}"
|
||||
|
||||
if [[ ! -d "$service_dir" ]]; then
|
||||
log_error "Service directory not found: $service_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${service_dir}/Dockerfile" ]]; then
|
||||
log_error "Dockerfile not found: ${service_dir}/Dockerfile"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would build and push: ${image_name}:${IMAGE_TAG}"
|
||||
else
|
||||
# Build image
|
||||
docker build \
|
||||
-t "${image_name}:${IMAGE_TAG}" \
|
||||
-t "${image_name}:latest-${ENVIRONMENT}" \
|
||||
"$service_dir"
|
||||
|
||||
# Push image
|
||||
docker push "${image_name}:${IMAGE_TAG}"
|
||||
docker push "${image_name}:latest-${ENVIRONMENT}"
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Container images built and pushed successfully"
|
||||
}
|
||||
|
||||
# Configure kubectl
|
||||
configure_kubectl() {
|
||||
log_info "Configuring kubectl for AKS cluster..."
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
az aks get-credentials \
|
||||
--resource-group "$AZURE_RESOURCE_GROUP" \
|
||||
--name "$AKS_CLUSTER_NAME" \
|
||||
--overwrite-existing
|
||||
fi
|
||||
|
||||
log_success "kubectl configured for $AKS_CLUSTER_NAME"
|
||||
}
|
||||
|
||||
# Deploy to Kubernetes
|
||||
deploy_to_kubernetes() {
|
||||
log_info "Deploying services to Kubernetes namespace: $AKS_NAMESPACE"
|
||||
|
||||
# Ensure namespace exists
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
kubectl create namespace "$AKS_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
|
||||
fi
|
||||
|
||||
for service in "${SERVICE_LIST[@]}"; do
|
||||
log_info "Deploying $service..."
|
||||
|
||||
local kustomize_path="${K8S_DIR}/${service}"
|
||||
local overlay_path="${kustomize_path}/${KUSTOMIZE_OVERLAY}"
|
||||
|
||||
if [[ ! -d "$overlay_path" ]]; then
|
||||
log_error "Kustomize overlay not found: $overlay_path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would deploy $service using: kubectl apply -k $overlay_path -n $AKS_NAMESPACE"
|
||||
else
|
||||
# Update image tag in kustomization.yaml if IMAGE_TAG is set
|
||||
if [[ -n "${IMAGE_TAG:-}" ]]; then
|
||||
local kustomization_file="${overlay_path}/kustomization.yaml"
|
||||
if [[ -f "$kustomization_file" ]]; then
|
||||
# Backup original file
|
||||
cp "$kustomization_file" "${kustomization_file}.backup"
|
||||
|
||||
# Update image tag
|
||||
sed -i "s|newTag:.*|newTag: ${IMAGE_TAG}|" "$kustomization_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Deploy using kubectl + kustomize
|
||||
kubectl apply -k "$overlay_path" -n "$AKS_NAMESPACE"
|
||||
|
||||
# Wait for rollout to complete
|
||||
kubectl rollout status deployment/"$service" -n "$AKS_NAMESPACE" --timeout=300s
|
||||
|
||||
# Restore backup if exists
|
||||
if [[ -f "${kustomization_file}.backup" ]]; then
|
||||
mv "${kustomization_file}.backup" "$kustomization_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Deployment completed successfully"
|
||||
}
|
||||
|
||||
# Perform health checks
|
||||
perform_health_checks() {
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "[DRY-RUN] Would perform health checks for deployed services"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Performing health checks..."
|
||||
|
||||
for service in "${SERVICE_LIST[@]}"; do
|
||||
log_info "Health checking $service..."
|
||||
|
||||
local max_retries=${HEALTH_CHECK_RETRY:-10}
|
||||
local retry_count=0
|
||||
local is_healthy=false
|
||||
|
||||
while [[ $retry_count -lt $max_retries ]]; do
|
||||
if kubectl get deployment "$service" -n "$AKS_NAMESPACE" -o json | \
|
||||
jq -e '.status.readyReplicas == .status.replicas and .status.replicas > 0' >/dev/null 2>&1; then
|
||||
is_healthy=true
|
||||
break
|
||||
fi
|
||||
|
||||
retry_count=$((retry_count + 1))
|
||||
log_info "Health check attempt $retry_count/$max_retries for $service..."
|
||||
sleep 30
|
||||
done
|
||||
|
||||
if [[ "$is_healthy" == true ]]; then
|
||||
log_success "$service is healthy"
|
||||
else
|
||||
log_error "$service failed health check"
|
||||
kubectl describe deployment "$service" -n "$AKS_NAMESPACE"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "All services passed health checks"
|
||||
}
|
||||
|
||||
# Show deployment summary
|
||||
show_summary() {
|
||||
log_info "========================================="
|
||||
log_info "Deployment Summary"
|
||||
log_info "========================================="
|
||||
log_info "Environment: $ENVIRONMENT"
|
||||
log_info "Namespace: $AKS_NAMESPACE"
|
||||
log_info "Services: ${SERVICE_LIST[*]}"
|
||||
|
||||
if [[ -n "${IMAGE_TAG:-}" ]]; then
|
||||
log_info "Image Tag: $IMAGE_TAG"
|
||||
fi
|
||||
|
||||
log_info "Options:"
|
||||
log_info " Skip Build: $SKIP_BUILD"
|
||||
log_info " Skip Test: $SKIP_TEST"
|
||||
log_info " Skip Push: $SKIP_PUSH"
|
||||
log_info " Force Deploy: $FORCE_DEPLOY"
|
||||
log_info " Dry Run: $DRY_RUN"
|
||||
log_info "========================================="
|
||||
}
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
log_error "Deployment failed with exit code: $exit_code"
|
||||
|
||||
# Show recent events for debugging
|
||||
if [[ "$DRY_RUN" == false ]] && command -v kubectl >/dev/null 2>&1; then
|
||||
log_info "Recent events in namespace $AKS_NAMESPACE:"
|
||||
kubectl get events -n "$AKS_NAMESPACE" --sort-by='.lastTimestamp' --field-selector type=Warning | tail -10
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution function
|
||||
main() {
|
||||
# Set up error handling
|
||||
trap cleanup EXIT
|
||||
|
||||
log_info "🚀 Starting phonebill deployment script"
|
||||
|
||||
# Parse arguments and validate
|
||||
parse_arguments "$@"
|
||||
show_summary
|
||||
|
||||
# Load configuration and validate prerequisites
|
||||
load_environment_config
|
||||
validate_prerequisites
|
||||
resolve_services
|
||||
|
||||
# Execute deployment steps
|
||||
build_services
|
||||
build_and_push_images
|
||||
configure_kubectl
|
||||
deploy_to_kubernetes
|
||||
perform_health_checks
|
||||
|
||||
log_success "🎉 Deployment completed successfully!"
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
log_info "You can check the deployment status with:"
|
||||
log_info " kubectl get pods -n $AKS_NAMESPACE"
|
||||
log_info " kubectl get services -n $AKS_NAMESPACE"
|
||||
log_info " kubectl get ingress -n $AKS_NAMESPACE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute main function if script is run directly
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
echo ""
|
||||
echo "✅ Deployment completed successfully!"
|
||||
echo ""
|
||||
echo "📊 Deployment Status:"
|
||||
kubectl get pods -n phonebill-${ENVIRONMENT} -l app=phonebill
|
||||
echo ""
|
||||
echo "🌐 Services:"
|
||||
kubectl get services -n phonebill-${ENVIRONMENT}
|
||||
echo ""
|
||||
echo "🔗 Ingress:"
|
||||
kubectl get ingress -n phonebill-${ENVIRONMENT}
|
||||
echo ""
|
||||
echo "🎯 Environment: ${ENVIRONMENT}"
|
||||
echo "🏷️ Image Tag: ${ENVIRONMENT}-${IMAGE_TAG}"
|
||||
Reference in New Issue
Block a user