From 27715efe7ce9034a2fb9a4c5cdecfff44ffe5aab Mon Sep 17 00:00:00 2001 From: cialloo Date: Sat, 4 Oct 2025 15:22:47 +0800 Subject: [PATCH] update --- .github/workflows/cd.yml | 74 +++++++++++++++++++ .github/workflows/ci.yml | 48 ++++++++++++ Dockerfile | 40 ++++++++++ script/cd.sh | 145 +++++++++++++++++++++++++++++++++++++ script/ci.sh | 112 ++++++++++++++++++++++++++++ script/k8s/deployment.yaml | 62 ++++++++++++++++ script/k8s/ingress.yaml | 25 +++++++ script/k8s/namespace.yaml | 7 ++ script/k8s/service.yaml | 19 +++++ 9 files changed, 532 insertions(+) create mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 script/cd.sh create mode 100644 script/ci.sh create mode 100644 script/k8s/deployment.yaml create mode 100644 script/k8s/ingress.yaml create mode 100644 script/k8s/namespace.yaml create mode 100644 script/k8s/service.yaml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..8a2d591 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,74 @@ +# Continuous Deployment Workflow +# This workflow deploys your application to Kubernetes cluster + +name: CD - Deploy to Kubernetes + +on: + workflow_dispatch: + inputs: + image_tag: + description: 'Docker image tag to deploy (e.g., latest, v1.0.0)' + required: false + default: 'latest' + type: string + namespace: + description: 'Kubernetes namespace (e.g., production, staging)' + required: false + default: 'default' + type: string + ingress_host: + description: 'Ingress host domain (e.g., stats-api.example.com)' + required: false + default: '' + type: string + +env: + # Kubernetes configuration + KUBECONFIG_DATA: ${{ secrets.KUBECONFIG_DATA }} + KUBERNETES_URL: ${{ secrets.KUBERNETES_URL }} + KUBERNETES_NAMESPACE: ${{ inputs.namespace || secrets.KUBERNETES_NAMESPACE }} + KUBERNETES_INGRESS_HOST: ${{ inputs.ingress_host || secrets.KUBERNETES_INGRESS_HOST }} + + # Container registry configuration + CONTAINER_REGISTRY_URL: ${{ secrets.CONTAINER_REGISTRY_URL }} + CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} + CONTAINER_REGISTRY_NAMESPACE: ${{ secrets.CONTAINER_REGISTRY_NAMESPACE }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + CONTAINER_IMAGE_NAME: stats-api + CONTAINER_IMAGE_TAG: ${{ inputs.image_tag || 'latest' }} + +jobs: + deploy: + name: Deploy to Kubernetes + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Make CD script executable + run: chmod +x script/cd.sh + + - name: Deploy to Kubernetes + run: ./script/cd.sh deploy + + - name: Deployment Summary + if: success() + run: | + echo "### :white_check_mark: Deployment Successful!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Application:** \`${CONTAINER_IMAGE_NAME}\`" >> $GITHUB_STEP_SUMMARY + echo "**Namespace:** \`${KUBERNETES_NAMESPACE}\`" >> $GITHUB_STEP_SUMMARY + echo "**Image:** \`${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}\`" >> $GITHUB_STEP_SUMMARY + echo "**URL:** http://${KUBERNETES_INGRESS_HOST}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Deployment Time:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY + + - name: Deployment Failed + if: failure() + run: | + echo "### :x: Deployment Failed!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Please check the logs above for error details." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d8b1544 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +# Continuous Integration Workflow +# This workflow builds and pushes Docker images to your private registry + +name: CI - Build and Push + +on: + workflow_dispatch: + inputs: + image_tag: + description: 'Docker image tag (e.g., latest, v1.0.0)' + required: false + default: 'latest' + type: string + +env: + CONTAINER_REGISTRY_URL: ${{ secrets.CONTAINER_REGISTRY_URL }} + CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} + CONTAINER_REGISTRY_NAMESPACE: ${{ secrets.CONTAINER_REGISTRY_NAMESPACE }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + CONTAINER_IMAGE_NAME: stats-api + CONTAINER_IMAGE_TAG: ${{ inputs.image_tag || 'latest' }} + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Make CI script executable + run: chmod +x script/ci.sh + + - name: Build Docker image + run: ./script/ci.sh build + + - name: Push Docker image + run: ./script/ci.sh push + + - name: Summary + run: | + echo "### :rocket: Build Complete!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Image:** \`${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Registry:** ${CONTAINER_REGISTRY_URL}" >> $GITHUB_STEP_SUMMARY + echo "**Tag:** ${CONTAINER_IMAGE_TAG}" >> $GITHUB_STEP_SUMMARY diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a2d3926 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Build stage +FROM golang:1.21-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git + +# Set working directory +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o stats-api stats.go + +# Production stage +FROM alpine:latest + +# Install ca-certificates for HTTPS +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +# Copy binary from builder +COPY --from=builder /app/stats-api . + +# Copy config file +COPY --from=builder /app/etc/stats-api.yaml ./etc/ + +# Expose port +EXPOSE 8888 + +# Run the application +CMD ["./stats-api"] diff --git a/script/cd.sh b/script/cd.sh new file mode 100644 index 0000000..42d74f6 --- /dev/null +++ b/script/cd.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +# Continuous Deployment Script for Kubernetes +# This script deploys your application to a Kubernetes cluster + +# ============================================================================= +# Environment Variables (with default values) +# ============================================================================= + +# Kubernetes Configuration +KUBECONFIG_DATA="${KUBECONFIG_DATA:-}" +KUBERNETES_URL="${KUBERNETES_URL:-https://kubernetes.default.svc}" +KUBERNETES_NAMESPACE="${KUBERNETES_NAMESPACE:-default}" +KUBERNETES_INGRESS_HOST="${KUBERNETES_INGRESS_HOST:-stats-api.example.com}" + +# Container Registry +CONTAINER_REGISTRY_URL="${CONTAINER_REGISTRY_URL:-127.0.0.1}" +CONTAINER_REGISTRY_USERNAME="${CONTAINER_REGISTRY_USERNAME:-username}" +CONTAINER_REGISTRY_NAMESPACE="${CONTAINER_REGISTRY_NAMESPACE:-username}" +CONTAINER_REGISTRY_PASSWORD="${CONTAINER_REGISTRY_PASSWORD:-password}" +CONTAINER_IMAGE_NAME="${CONTAINER_IMAGE_NAME:-stats-api}" +CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG:-latest}" + +# ============================================================================= +# Functions +# ============================================================================= + +# Print help message +print_help() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " deploy Deploy application to Kubernetes" + echo " help Show this help message (default)" +} + +# Setup kubectl configuration +setup_kubectl() { + echo "Setting up kubectl configuration..." + + if [ -z "${KUBECONFIG_DATA}" ]; then + echo "✗ KUBECONFIG_DATA is not set" + return 1 + fi + + mkdir -p ~/.kube + echo "${KUBECONFIG_DATA}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + echo "✓ kubectl configured" + return 0 +} + +# Create namespace if it doesn't exist +create_namespace() { + echo "Checking namespace: ${KUBERNETES_NAMESPACE}" + + kubectl get namespace "${KUBERNETES_NAMESPACE}" &> /dev/null + + if [ $? -ne 0 ]; then + echo "Creating namespace: ${KUBERNETES_NAMESPACE}" + kubectl create namespace "${KUBERNETES_NAMESPACE}" + else + echo "✓ Namespace exists: ${KUBERNETES_NAMESPACE}" + fi +} + +# Create image pull secret +create_image_pull_secret() { + echo "Creating image pull secret..." + + kubectl create secret docker-registry regcred \ + --docker-server="${CONTAINER_REGISTRY_URL}" \ + --docker-username="${CONTAINER_REGISTRY_USERNAME}" \ + --docker-password="${CONTAINER_REGISTRY_PASSWORD}" \ + --namespace="${KUBERNETES_NAMESPACE}" \ + --dry-run=client -o yaml | kubectl apply -f - + + echo "✓ Image pull secret created/updated" +} + +# Deploy to Kubernetes +deploy_to_kubernetes() { + FULL_IMAGE_NAME="${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}" + + echo "==========================================" + echo "Deploying to Kubernetes" + echo "==========================================" + echo "Namespace: ${KUBERNETES_NAMESPACE}" + echo "Image: ${FULL_IMAGE_NAME}" + echo "Host: ${KUBERNETES_INGRESS_HOST}" + echo "" + + # Setup kubectl + setup_kubectl || return 1 + + # Create namespace + create_namespace || return 1 + + # Create image pull secret + create_image_pull_secret || return 1 + + # Apply Kubernetes manifests with variable substitution + echo "Applying Kubernetes manifests..." + + export FULL_IMAGE_NAME + export KUBERNETES_NAMESPACE + export KUBERNETES_INGRESS_HOST + export CONTAINER_IMAGE_NAME + + for file in script/k8s/*.yaml; do + echo "Applying: $(basename $file)" + envsubst < "$file" | kubectl apply -f - + done + + echo "" + echo "✓ Deployment complete" + echo "" + echo "Waiting for rollout..." + kubectl rollout status deployment/${CONTAINER_IMAGE_NAME} -n ${KUBERNETES_NAMESPACE} --timeout=300s + + if [ $? -eq 0 ]; then + echo "" + echo "✓ Application is ready" + echo "URL: http://${KUBERNETES_INGRESS_HOST}" + return 0 + else + echo "" + echo "✗ Rollout failed or timed out" + return 1 + fi +} + +# ============================================================================= +# Main Script +# ============================================================================= + +case "${1:-help}" in + deploy) + deploy_to_kubernetes + ;; + help|*) + print_help + ;; +esac diff --git a/script/ci.sh b/script/ci.sh new file mode 100644 index 0000000..fc8e1e8 --- /dev/null +++ b/script/ci.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# CI/CD Script for Docker Build and Push +# This script builds a Docker image and pushes it to a private container registry + +# ============================================================================= +# Environment Variables (with default values) +# ============================================================================= +CONTAINER_REGISTRY_URL="${CONTAINER_REGISTRY_URL:-127.0.0.1}" +CONTAINER_REGISTRY_USERNAME="${CONTAINER_REGISTRY_USERNAME:-username}" +CONTAINER_REGISTRY_NAMESPACE="${CONTAINER_REGISTRY_NAMESPACE:-username}" +CONTAINER_REGISTRY_PASSWORD="${CONTAINER_REGISTRY_PASSWORD:-password}" +CONTAINER_IMAGE_NAME="${CONTAINER_IMAGE_NAME:-stats-api}" +CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG:-latest}" + +# ============================================================================= +# Functions +# ============================================================================= + +# Print help message +print_help() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " build Build Docker image" + echo " push Push Docker image to registry" + echo " help Show this help message (default)" + echo "" + echo "Environment Variables:" + echo " CONTAINER_REGISTRY_URL Registry URL (default: 127.0.0.1)" + echo " CONTAINER_REGISTRY_USERNAME Registry username (default: username)" + echo " CONTAINER_REGISTRY_NAMESPACE Registry namespace (default: username)" + echo " CONTAINER_REGISTRY_PASSWORD Registry password (default: password)" + echo " CONTAINER_IMAGE_NAME Image name (default: stats-api)" + echo " CONTAINER_IMAGE_TAG Image tag (default: latest)" +} + +# Build Docker image +build_image() { + FULL_IMAGE_NAME="${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}" + + echo "==========================================" + echo "Building Docker Image" + echo "==========================================" + echo "Image: ${FULL_IMAGE_NAME}" + echo "" + + docker build -t "${FULL_IMAGE_NAME}" . + + if [ $? -eq 0 ]; then + echo "" + echo "✓ Build successful: ${FULL_IMAGE_NAME}" + return 0 + else + echo "" + echo "✗ Build failed" + return 1 + fi +} + +# Push Docker image to registry +push_image() { + FULL_IMAGE_NAME="${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}" + + echo "==========================================" + echo "Pushing Docker Image" + echo "==========================================" + echo "Registry: ${CONTAINER_REGISTRY_URL}" + echo "Image: ${FULL_IMAGE_NAME}" + echo "" + + # Login to registry + echo "${CONTAINER_REGISTRY_PASSWORD}" | docker login "${CONTAINER_REGISTRY_URL}" \ + --username "${CONTAINER_REGISTRY_USERNAME}" \ + --password-stdin + + if [ $? -ne 0 ]; then + echo "✗ Registry login failed" + return 1 + fi + + # Push image + docker push "${FULL_IMAGE_NAME}" + + if [ $? -eq 0 ]; then + echo "" + echo "✓ Push successful: ${FULL_IMAGE_NAME}" + docker logout "${CONTAINER_REGISTRY_URL}" + return 0 + else + echo "" + echo "✗ Push failed" + docker logout "${CONTAINER_REGISTRY_URL}" + return 1 + fi +} + +# ============================================================================= +# Main Script +# ============================================================================= + +case "${1:-help}" in + build) + build_image + ;; + push) + push_image + ;; + help|*) + print_help + ;; +esac diff --git a/script/k8s/deployment.yaml b/script/k8s/deployment.yaml new file mode 100644 index 0000000..1b77b53 --- /dev/null +++ b/script/k8s/deployment.yaml @@ -0,0 +1,62 @@ +# Kubernetes Deployment Configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ${CONTAINER_IMAGE_NAME} + namespace: ${KUBERNETES_NAMESPACE} + labels: + app: ${CONTAINER_IMAGE_NAME} +spec: + replicas: 2 + + selector: + matchLabels: + app: ${CONTAINER_IMAGE_NAME} + + template: + metadata: + labels: + app: ${CONTAINER_IMAGE_NAME} + spec: + imagePullSecrets: + - name: regcred + + containers: + - name: ${CONTAINER_IMAGE_NAME} + image: ${FULL_IMAGE_NAME} + imagePullPolicy: Always + + ports: + - name: http + containerPort: 8888 + protocol: TCP + + readinessProbe: + httpGet: + path: /api/v1/player/stats?auth=1 + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + livenessProbe: + httpGet: + path: /api/v1/player/stats?auth=1 + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + + resources: + requests: + memory: "128Mi" + cpu: "200m" + limits: + memory: "256Mi" + cpu: "500m" + + env: + - name: TZ + value: "UTC" diff --git a/script/k8s/ingress.yaml b/script/k8s/ingress.yaml new file mode 100644 index 0000000..b9b9d9a --- /dev/null +++ b/script/k8s/ingress.yaml @@ -0,0 +1,25 @@ +# Kubernetes Ingress Configuration for Traefik +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ${CONTAINER_IMAGE_NAME} + namespace: ${KUBERNETES_NAMESPACE} + + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + + labels: + app: ${CONTAINER_IMAGE_NAME} + +spec: + rules: + - host: ${KUBERNETES_INGRESS_HOST} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ${CONTAINER_IMAGE_NAME} + port: + number: 8888 diff --git a/script/k8s/namespace.yaml b/script/k8s/namespace.yaml new file mode 100644 index 0000000..a3b1961 --- /dev/null +++ b/script/k8s/namespace.yaml @@ -0,0 +1,7 @@ +# Kubernetes Namespace Configuration +apiVersion: v1 +kind: Namespace +metadata: + name: ${KUBERNETES_NAMESPACE} + labels: + name: ${KUBERNETES_NAMESPACE} diff --git a/script/k8s/service.yaml b/script/k8s/service.yaml new file mode 100644 index 0000000..fe976a2 --- /dev/null +++ b/script/k8s/service.yaml @@ -0,0 +1,19 @@ +# Kubernetes Service Configuration +apiVersion: v1 +kind: Service +metadata: + name: ${CONTAINER_IMAGE_NAME} + namespace: ${KUBERNETES_NAMESPACE} + labels: + app: ${CONTAINER_IMAGE_NAME} +spec: + type: ClusterIP + + selector: + app: ${CONTAINER_IMAGE_NAME} + + ports: + - name: http + port: 8888 + targetPort: http + protocol: TCP