diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index c105b31..fbd9ef8 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,5 +1,64 @@ -# TODO name: Spring seminar CD + on: + push: + branches: + - main + workflow_dispatch: jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + picokosmos/springboot-seminar:${{ github.sha }} + picokosmos/springboot-seminar:latest + cache-from: type=registry,ref=picokosmos/springboot-seminar:latest + cache-to: type=inline + + - name: Setup kubectl + uses: azure/setup-kubectl@v3 + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Create/Update app secrets + run: | + kubectl create secret generic app-secrets \ + --from-literal=db-password=${{ secrets.DB_PASSWORD }} \ + --from-literal=redis-password=${{ secrets.REDIS_PASSWORD }} \ + --from-literal=jwt-secret=${{ secrets.JWT_SECRET }} \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Deploy to k3s + run: | + kubectl set image deployment/springboot-app \ + springboot-app=picokosmos/springboot-seminar:${{ github.sha }} \ + --record + kubectl rollout status deployment/springboot-app --timeout=5m + + - name: Verify deployment + run: | + kubectl get pods -l app=springboot-app + kubectl get svc springboot-service diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3247e6e..eee1e4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,35 @@ -# TODO name: Spring seminar CI + on: + push: + branches: + - main + pull_request: + branches: + - main jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Build with Gradle + run: ./gradlew build -x test + + - name: Run tests + run: ./gradlew test + + - name: Build Docker image + run: docker build -t picokosmos/springboot-seminar:${{ github.sha }} . \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4640904..8bf22f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1 +1,31 @@ -# TODO +# Multi-stage build for Spring Boot application +FROM gradle:8.5-jdk17 AS build + +WORKDIR /app + +# Copy gradle files for dependency caching +COPY build.gradle.kts settings.gradle.kts ./ +COPY gradle ./gradle + +# Download dependencies +RUN gradle dependencies --no-daemon || true + +# Copy source code +COPY src ./src + +# Build the application +RUN gradle bootJar --no-daemon -x test + +# Runtime stage +FROM openjdk:17-slim + +WORKDIR /app + +# Copy the built jar from build stage +COPY --from=build /app/build/libs/*.jar app.jar + +# Expose port 8080 +EXPOSE 8080 + +# Run the application +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..b7031db --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: springboot-app + labels: + app: springboot-app +spec: + replicas: 3 + selector: + matchLabels: + app: springboot-app + template: + metadata: + labels: + app: springboot-app + spec: + containers: + - name: springboot-app + image: picokosmos/springboot-seminar:latest + ports: + - containerPort: 8080 + env: + - name: DB_HOST + value: my-mysql.default.svc.cluster.local + - name: DB_PORT + value: "3306" + - name: DB_NAME + value: "appdb" + - name: DB_USER + value: "root" + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: app-secrets + key: db-password + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: app-secrets + key: jwt-secret + - name: JWT_EXPIRATION_IN_MS + value: "86400000" + - name: REDIS_HOST + value: my-redis-master.default.svc.cluster.local + - name: REDIS_PORT + value: "6379" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: app-secrets + key: redis-password + livenessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 5 + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..4cf08f0 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: springboot-ingress +spec: + ingressClassName: traefik + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: springboot-service + port: + number: 8080 \ No newline at end of file diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..78f45d4 --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: springboot-service + labels: + app: springboot-app +spec: + type: NodePort + selector: + app: springboot-app + ports: + - port: 8080 + targetPort: 8080 + nodePort: 30080 + protocol: TCP + name: http \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 88d3e6f..3df661e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,15 +1,20 @@ spring: datasource: - url: jdbc:mysql://localhost:3306/seminar2025?allowPublicKeyRetrieval=true - username: user - password: somepassword + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?allowPublicKeyRetrieval=true + username: ${DB_USER} + password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + password: ${REDIS_PASSWORD} flyway: enabled: true jwt: - secret: this-is-a-very-long-and-secure-secret-key-for-jwt-token-generation - expiration-in-ms: 86400000 + secret: ${JWT_SECRET:this-is-a-very-long-and-secure-secret-key-for-jwt-token-generation} + expiration-in-ms: ${JWT_EXPIRATION_IN_MS:86400000} logging: level: