├── .circleci └── config.yml ├── .gitignore ├── .gitlab-ci.yml ├── .m2 └── settings.xml ├── Dockerfile ├── Jenkinsfile ├── Tiltfile ├── k8s ├── deployment-template.yaml ├── deployment.yaml ├── gitlab-serviceaccount.yaml ├── gitlab.yaml ├── helm-config.yaml ├── hpa.yaml ├── jenkins-agent-pvc.yaml ├── jenkins-helm-config.yaml ├── kind-cluster-test.yaml ├── knative-service.yaml ├── load-tests.js └── mongodb-deployment.yaml ├── okteto.yml ├── pom.xml ├── readme.md ├── renovate.json ├── skaffold.yaml └── src ├── main ├── java │ └── pl │ │ └── piomin │ │ └── samples │ │ └── springboot │ │ └── kubernetes │ │ ├── SpringBootOnKubernetesApp.java │ │ ├── controller │ │ └── PersonController.java │ │ ├── domain │ │ ├── Gender.java │ │ ├── Person.java │ │ └── PersonV2.java │ │ ├── repository │ │ └── PersonRepository.java │ │ └── service │ │ └── PersonService.java └── resources │ └── application.yml └── test ├── java └── pl │ └── piomin │ └── samples │ └── springboot │ └── kubernetes │ ├── MongoDBContainerDevMode.java │ ├── PersonControllerTest.java │ └── SpringBootOnKubernetesAppTest.java └── resources ├── application.yml └── k6 ├── load-tests-add.js └── load-tests-get-by-age.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: 'cimg/openjdk:21.0.6' 7 | steps: 8 | - checkout 9 | - run: 10 | name: Analyze on SonarCloud 11 | command: mvn verify sonar:sonar -DskipTests 12 | test: 13 | executor: machine_executor_amd64 14 | steps: 15 | - checkout 16 | - run: 17 | name: Install OpenJDK 21 18 | command: | 19 | java -version 20 | sudo apt-get update && sudo apt-get install openjdk-21-jdk 21 | sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java 22 | sudo update-alternatives --set javac /usr/lib/jvm/java-21-openjdk-amd64/bin/javac 23 | java -version 24 | export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 25 | - run: 26 | name: Maven Tests 27 | command: mvn test 28 | deploy-k8s: 29 | executor: machine_executor_amd64 30 | steps: 31 | - checkout 32 | - run: 33 | name: Install Kubectl 34 | command: | 35 | curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" 36 | chmod +x kubectl 37 | sudo mv ./kubectl /usr/local/bin/kubectl 38 | - run: 39 | name: Install Skaffold 40 | command: | 41 | curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 42 | chmod +x skaffold 43 | sudo mv skaffold /usr/local/bin 44 | - run: 45 | name: Install Kind 46 | command: | 47 | [ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64 48 | chmod +x ./kind 49 | sudo mv ./kind /usr/local/bin/kind 50 | - run: 51 | name: Install Grafana K6 52 | command: | 53 | sudo gpg -k 54 | sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 55 | echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list 56 | sudo apt-get update 57 | sudo apt-get install k6 58 | - run: 59 | name: Install OpenJDK 21 60 | command: | 61 | java -version 62 | sudo apt-get update && sudo apt-get install openjdk-21-jdk 63 | sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java 64 | sudo update-alternatives --set javac /usr/lib/jvm/java-21-openjdk-amd64/bin/javac 65 | java -version 66 | export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 67 | - run: 68 | name: Create Kind Cluster 69 | command: | 70 | kind create cluster --name c1 --config k8s/kind-cluster-test.yaml 71 | - run: 72 | name: Deploy to K8s 73 | command: | 74 | export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 75 | skaffold run 76 | - run: 77 | name: Run K6 Test 78 | command: | 79 | kubectl get svc 80 | k6 run src/test/resources/k6/load-tests-add.js -e PERSONS_URI=http://localhost:30000 81 | - run: 82 | name: Delete Kind Cluster 83 | command: | 84 | kind delete cluster --name c1 85 | 86 | executors: 87 | machine_executor_amd64: 88 | machine: 89 | image: ubuntu-2204:2023.10.1 90 | environment: 91 | architecture: "amd64" 92 | platform: "linux/amd64" 93 | 94 | workflows: 95 | maven_test: 96 | jobs: 97 | - test 98 | - build: 99 | context: SonarCloud 100 | - deploy-k8s: 101 | requires: 102 | - test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /target/ 3 | .odo/env 4 | .odo/odo-file-index.json -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: maven:latest 2 | 3 | stages: 4 | - build 5 | - test 6 | - image-build 7 | - deploy-tb 8 | - deploy-prod 9 | 10 | build: 11 | stage: build 12 | script: 13 | - mvn compile 14 | 15 | test: 16 | stage: test 17 | script: 18 | - mvn test 19 | 20 | image-build: 21 | stage: image-build 22 | script: 23 | - mvn -s .m2/settings.xml -P jib compile jib:build 24 | 25 | deploy-tb: 26 | image: bitnami/kubectl:latest 27 | stage: deploy 28 | only: 29 | - master 30 | script: 31 | - kubectl apply -f k8s/deployment.yaml -n test 32 | 33 | deploy-prod: 34 | image: bitnami/kubectl:latest 35 | stage: deploy 36 | only: 37 | - master 38 | when: manual 39 | script: 40 | - kubectl apply -f k8s/deployment.yaml -n prod -------------------------------------------------------------------------------- /.m2/settings.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | registry-1.docker.io 6 | ${DOCKER_LOGIN} 7 | ${DOCKER_PASSWORD} 8 | 9 | 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.8.6-openjdk-11 as build 2 | WORKDIR /workspace/app 3 | 4 | COPY pom.xml . 5 | 6 | RUN mvn -B -e -C -T 1C org.apache.maven.plugins:maven-dependency-plugin:3.0.2:go-offline 7 | 8 | COPY . . 9 | RUN mvn clean package -Dmaven.test.skip=true 10 | 11 | 12 | FROM openjdk:21-buster 13 | VOLUME /tmp 14 | ARG DEPENDENCY=/workspace/app/target/dependency 15 | COPY --from=build /workspace/app/target/sample-spring-boot-on-kubernetes-1.3-SNAPSHOT.jar app.jar 16 | ENTRYPOINT ["java","-jar", "app.jar"] -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | label "default" 4 | } 5 | stages { 6 | stage('Checkout') { 7 | steps { 8 | script { 9 | git url: 'https://github.com/piomin/sample-spring-boot-on-kubernetes.git', credentialsId: 'github_credentials' 10 | sh 'ls -la' 11 | } 12 | } 13 | } 14 | stage('Build') { 15 | agent { 16 | label "maven" 17 | } 18 | steps { 19 | sh 'ls -la' 20 | sh 'mvn -version' 21 | sh 'mvn clean compile' 22 | } 23 | } 24 | stage('Test') { 25 | agent { 26 | label "maven" 27 | } 28 | steps { 29 | sh 'mvn test' 30 | } 31 | } 32 | stage('Image') { 33 | agent { 34 | label "maven" 35 | } 36 | steps { 37 | sh 'mvn -P jib -Djib.to.auth.username=piomin -Djib.to.auth.password=Piot_123 compile jib:build' 38 | } 39 | } 40 | stage('Deploy on test') { 41 | steps { 42 | script { 43 | env.PIPELINE_NAMESPACE = "test" 44 | kubernetesDeploy kubeconfigId: 'docker-desktop', configs: 'k8s/deployment-template.yaml' 45 | } 46 | } 47 | } 48 | stage('Deploy on prod') { 49 | steps { 50 | script { 51 | env.PIPELINE_NAMESPACE = "prod" 52 | kubernetesDeploy kubeconfigId: 'docker-desktop', configs: 'k8s/deployment-template.yaml' 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | custom_build('piomin/sample-spring-boot-on-kubernetes', 2 | 'mvn package jib:dockerBuild -Pjib -Djib.to.image=$EXPECTED_REF', 3 | deps=['src']) 4 | k8s_yaml(['k8s/mongodb-deployment.yaml', 'k8s/deployment.yaml']) 5 | k8s_resource('sample-spring-boot-on-kubernetes-deployment', 6 | port_forwards=8000) -------------------------------------------------------------------------------- /k8s/deployment-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: sample-spring-boot-on-kubernetes-deployment 5 | namespace: ${PIPELINE_NAMESPACE} 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: sample-spring-boot-on-kubernetes 10 | template: 11 | metadata: 12 | annotations: 13 | prometheus.io/path: /actuator/prometheus 14 | prometheus.io/scrape: "true" 15 | prometheus.io/port: "8080" 16 | labels: 17 | app: sample-spring-boot-on-kubernetes 18 | spec: 19 | containers: 20 | - name: sample-spring-boot-on-kubernetes 21 | image: piomin/sample-spring-boot-on-kubernetes 22 | ports: 23 | - containerPort: 8080 24 | env: 25 | - name: MONGO_DATABASE 26 | valueFrom: 27 | configMapKeyRef: 28 | name: mongodb 29 | key: database-name 30 | - name: MONGO_USERNAME 31 | valueFrom: 32 | secretKeyRef: 33 | name: mongodb 34 | key: database-user 35 | - name: MONGO_PASSWORD 36 | valueFrom: 37 | secretKeyRef: 38 | name: mongodb 39 | key: database-password 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: sample-spring-boot-on-kubernetes-service 45 | namespace: ${PIPELINE_NAMESPACE} 46 | spec: 47 | type: NodePort 48 | selector: 49 | app: sample-spring-boot-on-kubernetes 50 | ports: 51 | - port: 8080 52 | -------------------------------------------------------------------------------- /k8s/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: sample-spring-boot-on-kubernetes-deployment 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: sample-spring-boot-on-kubernetes 9 | template: 10 | metadata: 11 | annotations: 12 | prometheus.io/path: /actuator/prometheus 13 | prometheus.io/scrape: "true" 14 | prometheus.io/port: "8080" 15 | labels: 16 | app: sample-spring-boot-on-kubernetes 17 | spec: 18 | containers: 19 | - name: sample-spring-boot-on-kubernetes 20 | image: piomin/sample-spring-boot-on-kubernetes 21 | ports: 22 | - containerPort: 8080 23 | env: 24 | - name: MONGO_DATABASE 25 | valueFrom: 26 | configMapKeyRef: 27 | name: mongodb 28 | key: database-name 29 | - name: MONGO_USERNAME 30 | valueFrom: 31 | secretKeyRef: 32 | name: mongodb 33 | key: database-user 34 | - name: MONGO_PASSWORD 35 | valueFrom: 36 | secretKeyRef: 37 | name: mongodb 38 | key: database-password 39 | - name: MONGO_URL 40 | value: mongodb 41 | readinessProbe: 42 | httpGet: 43 | port: 8080 44 | path: /readiness 45 | scheme: HTTP 46 | timeoutSeconds: 1 47 | periodSeconds: 10 48 | successThreshold: 1 49 | failureThreshold: 3 50 | resources: 51 | limits: 52 | memory: 1024Mi 53 | --- 54 | apiVersion: v1 55 | kind: Service 56 | metadata: 57 | name: sample-spring-boot-on-kubernetes-service 58 | spec: 59 | type: NodePort 60 | selector: 61 | app: sample-spring-boot-on-kubernetes 62 | ports: 63 | - port: 8080 64 | nodePort: 30000 65 | -------------------------------------------------------------------------------- /k8s/gitlab-serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: gitlab 5 | namespace: kube-system 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1beta1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: gitlab-admin 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: cluster-admin 15 | subjects: 16 | - kind: ServiceAccount 17 | name: gitlab 18 | namespace: kube-system 19 | -------------------------------------------------------------------------------- /k8s/gitlab.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: gitlab-deployment 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: gitlab 9 | template: 10 | metadata: 11 | labels: 12 | app: gitlab 13 | spec: 14 | containers: 15 | - name: gitlab 16 | image: gitlab/gitlab-ee 17 | env: 18 | - name: GITLAB_OMNIBUS_CONFIG 19 | value: "external_url 'http://gitlab-service.default/';gitlab_rails['registry_enabled'] = true;gitlab_rails['registry_api_url'] = \"http://172.17.0.2:5000\"" 20 | ports: 21 | - containerPort: 80 22 | name: http 23 | volumeMounts: 24 | - mountPath: /var/opt/gitlab 25 | name: data 26 | volumes: 27 | - name: data 28 | emptyDir: {} 29 | --- 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | name: gitlab-service 34 | spec: 35 | type: NodePort 36 | selector: 37 | app: gitlab 38 | ports: 39 | - port: 80 40 | targetPort: 80 41 | name: http 42 | -------------------------------------------------------------------------------- /k8s/helm-config.yaml: -------------------------------------------------------------------------------- 1 | prometheus: 2 | url: http://prometheus-server.default.svc 3 | port: 80 4 | path: "" 5 | 6 | rules: 7 | default: true 8 | custom: 9 | - seriesQuery: '{__name__=~"^http_server_requests_seconds_.*"}' 10 | resources: 11 | overrides: 12 | kubernetes_namespace: 13 | resource: namespace 14 | kubernetes_pod_name: 15 | resource: pod 16 | name: 17 | matches: "^http_server_requests_seconds_count(.*)" 18 | as: "http_server_requests_seconds_count_sum" 19 | metricsQuery: sum(<<.Series>>{<<.LabelMatchers>>,uri=~"/persons.*"}) by (<<.GroupBy>>) -------------------------------------------------------------------------------- /k8s/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2beta2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: sample-hpa 5 | namespace: default 6 | spec: 7 | scaleTargetRef: 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | name: sample-spring-boot-on-kubernetes-deployment 11 | minReplicas: 1 12 | maxReplicas: 10 13 | metrics: 14 | - type: Pods 15 | pods: 16 | metric: 17 | name: http_server_requests_seconds_count_sum 18 | target: 19 | type: AverageValue 20 | averageValue: 100 -------------------------------------------------------------------------------- /k8s/jenkins-agent-pvc.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: jenkins-agent 5 | spec: 6 | accessModes: 7 | - ReadWriteMany 8 | resources: 9 | requests: 10 | storage: 2Gi 11 | storageClassName: hostpath -------------------------------------------------------------------------------- /k8s/jenkins-helm-config.yaml: -------------------------------------------------------------------------------- 1 | agent: 2 | podName: default 3 | customJenkinsLabels: default 4 | volumes: 5 | - type: PVC 6 | claimName: jenkins-agent 7 | mountPath: /home/jenkins 8 | readOnly: false 9 | 10 | additionalAgents: 11 | maven: 12 | podName: maven 13 | customJenkinsLabels: maven 14 | image: jenkins/jnlp-agent-maven 15 | tag: jdk11 16 | volumes: 17 | - type: PVC 18 | claimName: jenkins-agent 19 | mountPath: /home/jenkins 20 | readOnly: false 21 | resources: 22 | limits: 23 | cpu: "1" 24 | memory: "2048Mi" 25 | 26 | master: 27 | JCasC: 28 | configScripts: 29 | creds: | 30 | credentials: 31 | system: 32 | domainCredentials: 33 | - domain: 34 | name: "github.com" 35 | description: "GitHub domain" 36 | specifications: 37 | - hostnameSpecification: 38 | includes: "github.com" 39 | credentials: 40 | - usernamePassword: 41 | scope: GLOBAL 42 | id: github_credentials 43 | username: piomin 44 | password: Piot_123 45 | - credentials: 46 | - kubeconfig: 47 | id: "docker-desktop" 48 | kubeconfigSource: 49 | directEntry: 50 | content: |- 51 | apiVersion: v1 52 | kind: Config 53 | preferences: {} 54 | clusters: 55 | - cluster: 56 | certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM1ekNDQWMrZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJd01URXdPVEE1TkRjd04xb1hEVE13TVRFd056QTVORGN3TjFvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTStrCnhKd3l6UVEydXNvRHh5RmwxREdwZWZQTVA0RGFVaVJsK01SQ1p1S0NFWUFkL0ZQOWtFS0RlVXMydmVURi9jMXYKUjZpTDlsMVMvdmN6REoyRXRuZUd0TXVPdWFXNnFCWkN5OFJ2NmFReHd0UEpnWVZGTHBNM2dXYmhqTHp3RXplOApEQlhxekZDZkNobXl3SkdORVdWV0s4VnBuSlpMbjRVbUZKcE5RQndsSXZwRC90UDJVUVRiRGNHYURqUE5vY2c0Cms1SmNOc3h3SDV0NkhIN0JZMW9jTEFLUUhsZ2V4V2ZObWdRRkM2UUcrRVNsWkpDVEtNdVVuM2JsRWVlYytmUWYKaVk3YmdOb3BjRThrS0lUY2pzMG95dGVyNmxuY2ZLTVBqSnc2RTNXMmpXRThKU2Z2WDE2dGVhZUZISDEyTmRqWgpWTER2ZWc3eVBsTlRmRVJld25FQ0F3RUFBYU5DTUVBd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZMWjUzVEhBSXp0bHljV0NrS1hhY2l4K0Y5a1FNQTBHQ1NxR1NJYjMKRFFFQkN3VUFBNElCQVFBZWllMTRoSlZkTHF3bUY0SGVPS0ZhNXVUYUt6aXRUSElJNHJhU3cxTUppZElQTmZERwprRk5KeXM1M2I4MHMveWFXQ3BPbXdCK1dEak9hWmREQjFxcXVxU1FxeGhkNWMxU2FBZ1VXTGp5OXdLa1dPMzBTCjB3RTRlVkY3Q1c5VGpSMEoyVXV6UEVXdFBKRWF4U2xKMGhuZlQyeFYvb0N5OE9kMm9EZjZkSFRvbE5UTUEyalcKZjRZdXl3U1Z5a2RNaXZYMU5xZzdmK3RrcEVwb25PdkQ4ZmFEL2dXZmpaWHNFdHo4NXRNcTVLd2NQNUh2ZDJ0ZgoyKzBSbEtFT0pyY1dyL1lEc2w3dWdDdkFJTVk4WGdJL1E5dTZZTjAzTngzWXdSS2UrMElpSzcyOHVuNVJaVEVXCmNZNHc0YkpudlN6WWpKeUJIaHNiQVNTNzN6NndXVEo4REhKSwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== 57 | server: https://kubernetes.default 58 | name: docker-desktop 59 | contexts: 60 | - context: 61 | cluster: docker-desktop 62 | user: docker-desktop 63 | name: docker-desktop 64 | current-context: docker-desktop 65 | users: 66 | - name: docker-desktop 67 | user: 68 | client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURGVENDQWYyZ0F3SUJBZ0lJRnh2QzMyK2tPMEl3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TURFeE1Ea3dPVFEzTURkYUZ3MHlNVEV4TURrd09UUTNNekZhTURZeApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sc3dHUVlEVlFRREV4SmtiMk5yWlhJdFptOXlMV1JsCmMydDBiM0F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ3M0TXdUU3ByMkRoOTMKTlpERldsNWQyaWgwbllBdTJmTk1RYjZ2ZHR5RUVpTUVpNk5BM05qRGM4OWl5WUhOU2J4YmVNNlNUMzRlTFIwaQpXbHJJSlhhVjNBSXhnbFo4SkdqczVUSHRlM1FjNXZVSkJJWXhndFJFTFlJMGlJekpZdEhoU1NwMFU0eWNjdzl5CnVGSm1YTHVBRVdXR0tTcitVd2Y3RWtuWmJoaFRNQWI0RUF1NlR6dkpyRHhpTDAzU0UrSWhJMTJDV1Y3cVRqZ1gKdGI1OXdKcWkwK3ZJSDBSc3dxOUpnemtQTUhLNkFBZkgxbmFmZ3VCQjM2VEppcUR6YWFxV2VZTmludlIrSkVHMAptakV3NWlFN3JHdUgrZVBxSklvdTJlc1YvN1hyYWx2UEl2Zng2ajFvRWI4NWtna2RuV0JiQlNmTmJCdnhHQU1uCmdnLzdzNHdoQWdNQkFBR2pTREJHTUE0R0ExVWREd0VCL3dRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQWZCZ05WSFNNRUdEQVdnQlMyZWQweHdDTTdaY25GZ3BDbDJuSXNmaGZaRURBTkJna3Foa2lHOXcwQgpBUXNGQUFPQ0FRRUFpbUg1c1JqcjB6WjlDRkU2SVVwNVRwV2pBUXhma29oQkpPOUZmRGE3N2kvR1NsYm1jcXFrCldiWUVYRkl3MU9EbFVjUy9QMXZ5d3ZwV0Y0VXNXTGhtYkF5ZkZGbXZRWFNEZHhYbTlkamI2OEVtRWFPSlI1VTYKOHJOTkR0TUVEY25sbFd2Qk1CRXBNbkNtcm9KcXo3ZzVzeDFQSmhwcDBUdUZDQTIwT2FXb3drTUNNUXRIZlhLQgpVUDA2eGxRU2o1SGNOS1BSQWFyQzBtSzZPVUhybExBcUIvOCtDQlowVUY2MXhTTGN1WFJvYU52S1ZDWHZnQy9kCkQ4ckxuWXFmbWl6WHMvcHJ3dEhsaVFBR2lmemU1MmttbTkyR2RrS2V1SmFRbmM5RWwrd2RZaUVBSHVKU1YvK04Kc2VRelpTa0ZmT2ozbHUxdWtoSDg4dGcxUUp2TkpuM1FhQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K 69 | client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBck9ETUUwcWE5ZzRmZHpXUXhWcGVYZG9vZEoyQUx0bnpURUcrcjNiY2hCSWpCSXVqClFOell3M1BQWXNtQnpVbThXM2pPa2s5K0hpMGRJbHBheUNWMmxkd0NNWUpXZkNSbzdPVXg3WHQwSE9iMUNRU0cKTVlMVVJDMkNOSWlNeVdMUjRVa3FkRk9NbkhNUGNyaFNabHk3Z0JGbGhpa3EvbE1IK3hKSjJXNFlVekFHK0JBTAp1azg3eWF3OFlpOU4waFBpSVNOZGdsbGU2azQ0RjdXK2ZjQ2FvdFByeUI5RWJNS3ZTWU01RHpCeXVnQUh4OVoyCm40TGdRZCtreVlxZzgybXFsbm1EWXA3MGZpUkJ0Sm94TU9ZaE82eHJoL25qNmlTS0x0bnJGZisxNjJwYnp5TDMKOGVvOWFCRy9PWklKSFoxZ1d3VW56V3diOFJnREo0SVArN09NSVFJREFRQUJBb0lCQVFDWEZZTGtYVEFlVit0aAo2RnRVVG96b0lxOTJjdXRDaHRHZFZGdk14dWtqTnlLSloydk9WUFBQcE5lYXN4YVFqWjlpcGFxS3JaUS8xUmVBCkhVejNXOTVPUzg5UzYyQ2Y3OFlQT3FLdXRGU2VxYTErS3drSUhobGFXQmRSeUFDYVE1VysrSTEweWt1NXNzak8KYm8zOHpaQkQ5WEF2bHF6dlJTdFZYZjlTV1doQzBlWnRKTm84QU4yZnpkdkRjUUgwOVRsejh1S05EaUNra2RYQQpHTTdZTUdoQktYWGd6YlcxSUVMejRlRUpDZDh0dklReitwcWtxRktIcHRjNnVJY1hLQjFxUGVGRDRSMm9iNUlNCnl5MUpBWlZyR0JHaUk5d1p5OFU1a253UW93emwwUTEwZXlRdUkwTG42SWthZG5SQktMRHcrczRGaE1UQVViOWYKT1NBR3JaVnRBb0dCQU9RTDJzSEN3T25KOW0xYmRiSlVLeTFsRHRsTmV4aDRKOGNERVZKR3NyRVNndlgrSi9ZZQpXb0NHZXc3cGNXakFWaWJhOUMzSFBSbEtOV2hXOExOVlpvUy9jQUxCa1lpSUZNdDlnT1NpZmtCOFhmeVJQT3hJCmNIc2ZjOXZ2OEFJcmxZVVpCNjI1ak8rUFZTOXZLOXZXUGtka3d0MlFSUHlqYlEwVS9mZmdvUWVIQW9HQkFNSVIKd0lqL3RVbmJTeTZzL1JXSlFiVmxPb1VPVjFpb0d4WmNOQjBkaktBV3YwUksvVHdldFBRSXh2dmd1d2RaVFdiTApSTGk5M3RPY3U0UXhNOFpkdGZhTnh5dWQvM2xXSHhPYzRZa0EwUHdSTzY4MjNMcElWNGxrc0tuNUN0aC80QTFJCmw3czV0bHVEbkI3dFdYTFM4MHcyYkE4YWtVZXlBbkZmWXpTTUR1a1hBb0dBSkRFaGNialg1d0t2Z21HT2gxUEcKV25qOFowNWRwOStCNkpxN0NBVENYVW5qME9pYUxQeGFQcVdaS0IreWFQNkZiYnM0SDMvTVdaUW1iNzNFaTZHVgpHS0pOUTVLMjV5VTVyNlhtYStMQ0NMZjBMcDVhUGVHdFFFMFlsU0k2UkEzb3Qrdm1CUk02bzlacW5aR1dNMWlJCkg4cUZCcWJiM0FDUDBSQ3cwY01ycTBjQ2dZRUFvMWM5cmhGTERMYStPTEx3OE1kdHZyZE00ZUNJTTk2SnJmQTkKREtScVQvUFZXQzJscG94UjBYUHh4dDRIak0vbERiZllSNFhIbm1RMGo3YTUxU1BhbTRJSk9QVHFxYjJLdW44NApkSTl6VmpWSy90WTJRYlBSdVpvOTkxSGRod3RhRU5RZ29UeVo5N3gyRXJIQ3I1cE5uTC9SZzRUZzhtOHBEek14CjFIQnR2RkVDZ1lFQTF5aHNPUDBRb3F2SHRNOUFuWVlua2JzQU12L2dqT3FrWUk5cjQ2c1V3Mnc3WHRJb1NTYlAKU0hmbGRxN0IxVXJoTmRJMFBXWXRWZ3kyV1NrN0FaeG8vUWtLZGtPbTAxS0pCY2xteW9JZDE0a0xCVkZxbUV6Rgp1c2l4MmpwdTVOTWhjUWo4aFY2Sk42aXdraHJkYjByZVpuMGo4MG1ZRE96d3hjMmpvTmxSWjN3PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= 70 | scope: GLOBAL -------------------------------------------------------------------------------- /k8s/kind-cluster-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kind.x-k8s.io/v1alpha4 2 | kind: Cluster 3 | nodes: 4 | - role: control-plane 5 | extraPortMappings: 6 | - containerPort: 30000 7 | hostPort: 30000 8 | listenAddress: "0.0.0.0" 9 | protocol: tcp -------------------------------------------------------------------------------- /k8s/knative-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | autoscaling.knative.dev/target: "20" 6 | autoscaling.knative.dev/metric: "rps" 7 | name: sample-spring-boot-on-kubernetes 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - image: piomin/sample-spring-boot-on-kubernetes 13 | livenessProbe: 14 | httpGet: 15 | path: /actuator/health/liveness 16 | readinessProbe: 17 | httpGet: 18 | path: /actuator/health/readiness 19 | env: 20 | - name: MONGO_DATABASE 21 | valueFrom: 22 | configMapKeyRef: 23 | name: mongodb 24 | key: database-name 25 | - name: MONGO_USERNAME 26 | valueFrom: 27 | secretKeyRef: 28 | name: mongodb 29 | key: database-user 30 | - name: MONGO_PASSWORD 31 | valueFrom: 32 | secretKeyRef: 33 | name: mongodb 34 | key: database-password -------------------------------------------------------------------------------- /k8s/load-tests.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { check } from 'k6'; 3 | 4 | export default function () { 5 | 6 | const payload = JSON.stringify({ 7 | firstName: 'aaa', 8 | lastName: 'bbb', 9 | age: 50, 10 | gender: 'MALE' 11 | }); 12 | 13 | const params = { 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | }; 18 | 19 | const res = http.post(`http://localhost:8080/persons`, payload, params); 20 | 21 | check(res, { 22 | 'is status 200': (res) => res.status === 200, 23 | 'body size is > 0': (r) => r.body.length > 0, 24 | }); 25 | } -------------------------------------------------------------------------------- /k8s/mongodb-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: mongodb 5 | data: 6 | database-name: admin 7 | --- 8 | apiVersion: v1 9 | kind: Secret 10 | metadata: 11 | name: mongodb 12 | type: Opaque 13 | data: 14 | database-password: dGVzdDEyMw== 15 | database-user: dGVzdA== 16 | --- 17 | apiVersion: apps/v1 18 | kind: Deployment 19 | metadata: 20 | name: mongodb 21 | labels: 22 | app: mongodb 23 | spec: 24 | replicas: 1 25 | selector: 26 | matchLabels: 27 | app: mongodb 28 | template: 29 | metadata: 30 | labels: 31 | app: mongodb 32 | spec: 33 | containers: 34 | - name: mongodb 35 | image: mongo:5.0 36 | ports: 37 | - containerPort: 27017 38 | env: 39 | - name: MONGO_INITDB_DATABASE 40 | valueFrom: 41 | configMapKeyRef: 42 | name: mongodb 43 | key: database-name 44 | - name: MONGO_INITDB_ROOT_USERNAME 45 | valueFrom: 46 | secretKeyRef: 47 | name: mongodb 48 | key: database-user 49 | - name: MONGO_INITDB_ROOT_PASSWORD 50 | valueFrom: 51 | secretKeyRef: 52 | name: mongodb 53 | key: database-password 54 | --- 55 | apiVersion: v1 56 | kind: Service 57 | metadata: 58 | name: mongodb 59 | labels: 60 | app: mongodb 61 | spec: 62 | ports: 63 | - port: 27017 64 | protocol: TCP 65 | selector: 66 | app: mongodb 67 | -------------------------------------------------------------------------------- /okteto.yml: -------------------------------------------------------------------------------- 1 | name: sample-spring-boot-on-kubernetes 2 | image: okteto/maven 3 | command: ["mvn", "spring-boot:run" ] 4 | workdir: /app 5 | environment: 6 | - MONGO_USERNAME=okteto 7 | - MONGO_DATABASE=okteto 8 | - MONGO_PASSWORD=okteto 9 | forward: 10 | - 8080:8080 11 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.5.0 11 | 12 | 13 | pl.piomin.samples 14 | sample-spring-boot-on-kubernetes 15 | 1.3-SNAPSHOT 16 | 17 | 18 | 21 19 | piomin_sample-spring-boot-on-kubernetes 20 | piomin 21 | https://sonarcloud.io 22 | 1.21.1 23 | 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-actuator 33 | 34 | 35 | io.micrometer 36 | micrometer-registry-prometheus 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-data-mongodb 41 | 42 | 43 | org.projectlombok 44 | lombok 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-devtools 49 | true 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-test 54 | test 55 | 56 | 57 | org.testcontainers 58 | mongodb 59 | test 60 | 61 | 62 | org.testcontainers 63 | junit-jupiter 64 | test 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-testcontainers 69 | test 70 | 71 | 72 | org.instancio 73 | instancio-junit 74 | 5.4.1 75 | test 76 | 77 | 78 | 79 | 80 | 81 | 82 | org.testcontainers 83 | testcontainers-bom 84 | ${testcontainers.version} 85 | pom 86 | import 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | org.springframework.boot 95 | spring-boot-maven-plugin 96 | 97 | 98 | 99 | build-info 100 | 101 | 102 | 103 | 104 | false 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.cyclonedx 112 | cyclonedx-maven-plugin 113 | 114 | 115 | org.jacoco 116 | jacoco-maven-plugin 117 | 0.8.13 118 | 119 | 120 | 121 | prepare-agent 122 | 123 | 124 | 125 | report 126 | test 127 | 128 | report 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | jib 139 | 140 | false 141 | 142 | 143 | 144 | 145 | com.google.cloud.tools 146 | jib-maven-plugin 147 | 3.4.5 148 | 149 | 150 | eclipse-temurin:21-jdk-ubi9-minimal 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Spring Boot on Kubernetes Demo Project [![Twitter](https://img.shields.io/twitter/follow/piotr_minkowski.svg?style=social&logo=twitter&label=Follow%20Me)](https://twitter.com/piotr_minkowski) 2 | 3 | [![CircleCI](https://circleci.com/gh/piomin/sample-spring-boot-on-kubernetes.svg?style=svg)](https://circleci.com/gh/piomin/sample-spring-boot-on-kubernetes) 4 | 5 | [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-black.svg)](https://sonarcloud.io/dashboard?id=piomin_sample-spring-boot-on-kubernetes) 6 | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-spring-boot-on-kubernetes&metric=bugs)](https://sonarcloud.io/dashboard?id=piomin_sample-spring-boot-on-kubernetes) 7 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-spring-boot-on-kubernetes&metric=coverage)](https://sonarcloud.io/dashboard?id=piomin_sample-spring-boot-on-kubernetes) 8 | [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-spring-boot-on-kubernetes&metric=ncloc)](https://sonarcloud.io/dashboard?id=piomin_sample-spring-boot-on-kubernetes) 9 | 10 | In this project I'm demonstrating different mechanisms of deploying application on Kubernetes. The example application application is simple Spring Boot app that exposes some HTTP endpoints for CRUD operations and connects to MongoDB on cluster. 11 | 12 | ## Getting Started 13 | Currently you may find here some examples of different techniques of deploying this application on Kubernetes. All the examples are described in a separated articles on my blog. Here's a full list of available examples: 14 | 1. Using [Okteto Cloud Platform](https://okteto.com/) - Kubernetes for Developers. A detailed guide may be find in the following article: [Development on Kubernetes with Okteto and Spring Boot](https://piotrminkowski.com/2020/06/15/development-on-kubernetes-with-okteto-and-spring-boot/) 15 | 16 | Mongo on Docker: 17 | ```shell 18 | $ docker run --name mongodb -d -p 27017:27017 \ 19 | -e MONGO_INITDB_ROOT_USERNAME=springboot \ 20 | -e MONGO_INITDB_ROOT_PASSWORD=springboot123 \ 21 | -e MONGO_INITDB_DATABASE=springboot \ 22 | mongo:latest 23 | ``` 24 | 25 | Mongo for OpenShift: 26 | ```shell 27 | $ oc new-app https://raw.githubusercontent.com/openshift/origin/master/examples/db-templates/mongodb-ephemeral-template.json 28 | ``` -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base",":dependencyDashboard" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | } 11 | ], 12 | "prCreation": "not-pending" 13 | } -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta22 2 | kind: Config 3 | metadata: 4 | name: sample-spring-boot-on-kubernetes 5 | build: 6 | artifacts: 7 | - image: piomin/sample-spring-boot-on-kubernetes 8 | jib: 9 | args: 10 | - -Pjib 11 | - -DskipTests 12 | deploy: 13 | kubectl: 14 | manifests: 15 | - k8s/mongodb-deployment.yaml 16 | - k8s/deployment.yaml 17 | profiles: 18 | - name: knative 19 | deploy: 20 | kubectl: 21 | manifests: 22 | - k8s/knative-service.yaml 23 | - name: buildpacks 24 | build: 25 | artifacts: 26 | - image: piomin/sample-spring-boot-on-kubernetes 27 | buildpacks: 28 | builder: paketobuildpacks/builder:base 29 | # builder: gcr.io/buildpacks/builder:v1 30 | # builder: heroku/buildpacks:20 31 | buildpacks: 32 | # - paketo-buildpacks/eclipse-openj9 33 | # - paketo-buildpacks/amazon-corretto 34 | # - paketo-buildpacks/oracle 35 | - paketo-buildpacks/adoptium 36 | # - paketo-buildpacks/azul-zulu 37 | # - paketo-buildpacks/alibaba-dragonwell 38 | # - paketo-buildpacks/microsoft-openjdk 39 | - paketo-buildpacks/java 40 | env: 41 | - BP_JVM_VERSION=17 42 | # - GOOGLE_RUNTIME_VERSION=17 43 | # tagPolicy: 44 | # envTemplate: 45 | # template: "{{.VENDOR}}" 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/samples/springboot/kubernetes/SpringBootOnKubernetesApp.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.event.ApplicationReadyEvent; 7 | import org.springframework.context.ApplicationListener; 8 | import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; 9 | import pl.piomin.samples.springboot.kubernetes.domain.Gender; 10 | import pl.piomin.samples.springboot.kubernetes.domain.Person; 11 | import pl.piomin.samples.springboot.kubernetes.repository.PersonRepository; 12 | 13 | @SpringBootApplication 14 | @EnableMongoRepositories 15 | public class SpringBootOnKubernetesApp implements ApplicationListener { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(SpringBootOnKubernetesApp.class, args); 19 | } 20 | 21 | @Autowired 22 | PersonRepository repository; 23 | 24 | @Override 25 | public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { 26 | if (repository.count() == 0) { 27 | repository.save(new Person(null, "XXX", "FFF", 20, Gender.MALE)); 28 | repository.save(new Person(null, "AAA", "EEE", 30, Gender.MALE)); 29 | repository.save(new Person(null, "ZZZ", "DDD", 40, Gender.FEMALE)); 30 | repository.save(new Person(null, "BBB", "CCC", 50, Gender.MALE)); 31 | repository.save(new Person(null, "YYY", "JJJ", 60, Gender.FEMALE)); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/samples/springboot/kubernetes/controller/PersonController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes.controller; 2 | 3 | import java.util.Optional; 4 | import java.util.Set; 5 | 6 | import pl.piomin.samples.springboot.kubernetes.domain.Gender; 7 | import pl.piomin.samples.springboot.kubernetes.domain.Person; 8 | import pl.piomin.samples.springboot.kubernetes.domain.PersonV2; 9 | import pl.piomin.samples.springboot.kubernetes.repository.PersonRepository; 10 | import pl.piomin.samples.springboot.kubernetes.service.PersonService; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.web.bind.annotation.DeleteMapping; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.PathVariable; 16 | import org.springframework.web.bind.annotation.PostMapping; 17 | import org.springframework.web.bind.annotation.PutMapping; 18 | import org.springframework.web.bind.annotation.RequestBody; 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RestController; 21 | 22 | @RestController 23 | @RequestMapping("/persons") 24 | public class PersonController { 25 | 26 | private PersonRepository repository; 27 | private PersonService service; 28 | 29 | PersonController(PersonRepository repository, PersonService service) { 30 | this.repository = repository; 31 | this.service = service; 32 | } 33 | 34 | @PostMapping 35 | public Person add(@RequestBody Person person) { 36 | return repository.save(person); 37 | } 38 | 39 | @PostMapping("/random") 40 | public Set add() { 41 | Person p1 = new Person(); 42 | p1.setAge(1); 43 | p1.setFirstName("X"); 44 | p1.setLastName("X"); 45 | p1.setGender(Gender.MALE); 46 | Person p2 = new Person(); 47 | p2.setAge(2); 48 | p2.setFirstName("Y"); 49 | p2.setLastName("Y"); 50 | p2.setGender(Gender.FEMALE); 51 | return service.doIt(p1, p2); 52 | } 53 | 54 | @PutMapping 55 | public Person update(@RequestBody Person person) { 56 | return repository.save(person); 57 | } 58 | 59 | @DeleteMapping("/{id}") 60 | public void delete(@PathVariable("id") String id) { 61 | repository.deleteById(id); 62 | } 63 | 64 | @GetMapping 65 | public Iterable findAll() { 66 | return repository.findAll(); 67 | } 68 | 69 | @GetMapping("/{id}") 70 | public Optional findById(@PathVariable("id") String id) { 71 | return repository.findById(id); 72 | } 73 | 74 | @GetMapping("/v2/{id}") 75 | public PersonV2 findByIdV2(@PathVariable("id") String id) { 76 | Person p = repository.findById(id).orElseThrow(); 77 | PersonV2 personV2 = new PersonV2(); 78 | personV2.setAge(p.getAge()); 79 | personV2.setGender(p.getGender()); 80 | personV2.setName(p.getFirstName() + " " + p.getLastName()); 81 | personV2.setId(p.getId()); 82 | return personV2; 83 | } 84 | 85 | @GetMapping("/first-name/{firstName}/last-name/{lastName}") 86 | public Set findByFirstNameAndLastName(@PathVariable("firstName") String firstName, 87 | @PathVariable("lastName") String lastName) { 88 | return repository.findByFirstNameAndLastName(firstName, lastName); 89 | } 90 | 91 | @GetMapping("/age-greater-than/{age}") 92 | public Set findByAgeGreaterThan(@PathVariable("age") int age) { 93 | return repository.findByAgeGreaterThan(age); 94 | } 95 | 96 | @GetMapping("/age/{age}") 97 | public Set findByAge(@PathVariable("age") int age) { 98 | return repository.findByAge(age); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/samples/springboot/kubernetes/domain/Gender.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes.domain; 2 | 3 | public enum Gender { 4 | 5 | MALE, FEMALE; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/samples/springboot/kubernetes/domain/Person.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import org.springframework.data.annotation.Id; 9 | import org.springframework.data.mongodb.core.mapping.Document; 10 | 11 | @Document(collection = "person") 12 | @Getter 13 | @Setter 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class Person { 17 | 18 | @Id 19 | private String id; 20 | private String firstName; 21 | private String lastName; 22 | private int age; 23 | private Gender gender; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/samples/springboot/kubernetes/domain/PersonV2.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import org.springframework.data.annotation.Id; 8 | import org.springframework.data.mongodb.core.mapping.Document; 9 | 10 | @Document(collection = "person") 11 | @Getter 12 | @Setter 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class PersonV2 { 16 | 17 | @Id 18 | private String id; 19 | private String name; 20 | private int age; 21 | private Gender gender; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/samples/springboot/kubernetes/repository/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes.repository; 2 | 3 | import java.util.Set; 4 | 5 | import pl.piomin.samples.springboot.kubernetes.domain.Person; 6 | 7 | import org.springframework.data.repository.CrudRepository; 8 | 9 | public interface PersonRepository extends CrudRepository { 10 | 11 | Set findByFirstNameAndLastName(String firstName, String lastName); 12 | Set findByAge(int age); 13 | Set findByAgeGreaterThan(int age); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/samples/springboot/kubernetes/service/PersonService.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes.service; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import pl.piomin.samples.springboot.kubernetes.domain.Person; 7 | import pl.piomin.samples.springboot.kubernetes.repository.PersonRepository; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationEventPublisher; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | @Service 15 | public class PersonService { 16 | 17 | @Autowired 18 | PersonRepository repository; 19 | @Autowired 20 | ApplicationEventPublisher applicationEventPublisher; 21 | 22 | @Transactional 23 | public Set doIt(Person... persons) { 24 | Set personSet = new HashSet<>(); 25 | for (Person p: persons) { 26 | personSet.add(repository.save(p)); 27 | } 28 | // applicationEventPublisher.publishEvent(new PersonAddEvent()); 29 | return personSet; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: sample-spring-boot-on-kubernetes 4 | data: 5 | mongodb: 6 | host: ${MONGO_URL} 7 | port: 27017 8 | username: ${MONGO_USERNAME} 9 | password: ${MONGO_PASSWORD} 10 | database: ${MONGO_DATABASE} 11 | authentication-database: admin 12 | #logging: 13 | # pattern: 14 | # console: "%d{yyyy-MM-dd HH:mm:ss} ${LOG_LEVEL_PATTERN:-%5p} %m%n" 15 | 16 | management: 17 | endpoints: 18 | web: 19 | exposure: 20 | include: "*" 21 | endpoint.health: 22 | show-details: always 23 | group: 24 | readiness: 25 | include: mongo 26 | additional-path: server:/readiness 27 | probes: 28 | enabled: true 29 | server: 30 | port: 8081 31 | 32 | #management.endpoint.health.group.live.additional-path="server:/healthz" 33 | 34 | spring.output.ansi.enabled: ALWAYS 35 | #logging.pattern.console: "%clr(%d{HH:mm:ss.SSS}){blue} %clr(%5p) %clr(---){cyan} %clr([%15.15t]){yellow} %clr(:){red} %clr(%m){magenta}%n" -------------------------------------------------------------------------------- /src/test/java/pl/piomin/samples/springboot/kubernetes/MongoDBContainerDevMode.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes; 2 | 3 | import org.springframework.boot.devtools.restart.RestartScope; 4 | import org.springframework.boot.test.context.TestConfiguration; 5 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 6 | import org.springframework.context.annotation.Bean; 7 | import org.testcontainers.containers.MongoDBContainer; 8 | 9 | @TestConfiguration 10 | public class MongoDBContainerDevMode { 11 | 12 | @Bean 13 | @ServiceConnection 14 | @RestartScope 15 | MongoDBContainer mongoDBContainer() { 16 | return new MongoDBContainer("mongo:5.0"); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/pl/piomin/samples/springboot/kubernetes/PersonControllerTest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes; 2 | 3 | import org.junit.jupiter.api.MethodOrderer; 4 | import org.junit.jupiter.api.Order; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.TestMethodOrder; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.client.TestRestTemplate; 10 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 11 | import org.testcontainers.containers.MongoDBContainer; 12 | import org.testcontainers.junit.jupiter.Container; 13 | import org.testcontainers.junit.jupiter.Testcontainers; 14 | import pl.piomin.samples.springboot.kubernetes.domain.Gender; 15 | import pl.piomin.samples.springboot.kubernetes.domain.Person; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertNotNull; 19 | 20 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 21 | @Testcontainers 22 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 23 | public class PersonControllerTest { 24 | 25 | private static String id; 26 | 27 | @Container 28 | @ServiceConnection 29 | static MongoDBContainer mongodb = new MongoDBContainer("mongo:5.0"); 30 | 31 | @Autowired 32 | TestRestTemplate restTemplate; 33 | 34 | @Test 35 | @Order(1) 36 | void add() { 37 | Person person = new Person(null, "Test", "Test", 100, Gender.FEMALE); 38 | Person personAdded = restTemplate.postForObject("/persons", person, Person.class); 39 | assertNotNull(personAdded); 40 | assertNotNull(personAdded.getId()); 41 | assertEquals(person.getLastName(), personAdded.getLastName()); 42 | id = personAdded.getId(); 43 | } 44 | 45 | @Test 46 | @Order(2) 47 | void findById() { 48 | Person person = restTemplate.getForObject("/persons/{id}", Person.class, id); 49 | assertNotNull(person); 50 | assertNotNull(person.getId()); 51 | assertEquals(id, person.getId()); 52 | } 53 | 54 | @Test 55 | @Order(2) 56 | void findAll() { 57 | Person[] persons = restTemplate.getForObject("/persons", Person[].class); 58 | assertEquals(6, persons.length); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/pl/piomin/samples/springboot/kubernetes/SpringBootOnKubernetesAppTest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.samples.springboot.kubernetes; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | 5 | public class SpringBootOnKubernetesAppTest { 6 | 7 | public static void main(String[] args) { 8 | SpringApplication.from(SpringBootOnKubernetesApp::main) 9 | .with(MongoDBContainerDevMode.class) 10 | .run(args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: sample-spring-boot-on-kubernetes 4 | data: 5 | mongodb: 6 | uri: mongodb://localhost:27071/test 7 | logging: 8 | pattern: 9 | console: "%d{yyyy-MM-dd HH:mm:ss} ${LOG_LEVEL_PATTERN:-%5p} %m%n" 10 | 11 | management: 12 | endpoints: 13 | web: 14 | exposure: 15 | include: "*" 16 | endpoint: 17 | health: 18 | show-details: ALWAYS -------------------------------------------------------------------------------- /src/test/resources/k6/load-tests-add.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { check } from 'k6'; 3 | import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; 4 | import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; 5 | 6 | export const options = { 7 | duration: '60s', 8 | vus: 5, 9 | thresholds: { 10 | http_req_failed: ['rate<0.25'], 11 | http_req_duration: ['p(95)<1000'], 12 | }, 13 | }; 14 | 15 | export default function () { 16 | 17 | const payload = JSON.stringify({ 18 | firstName: randomString(6), 19 | lastName: randomString(6), 20 | age: randomIntBetween(10, 50), 21 | gender: 'MALE' 22 | }); 23 | 24 | const params = { 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | }; 29 | 30 | const res = http.post(`${__ENV.PERSONS_URI}/persons`, payload, params); 31 | 32 | check(res, { 33 | 'is status 200': (res) => res.status === 200, 34 | 'body size is > 0': (r) => r.body.length > 0, 35 | }); 36 | } -------------------------------------------------------------------------------- /src/test/resources/k6/load-tests-get-by-age.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { check } from 'k6'; 3 | import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; 4 | 5 | export default function () { 6 | const age = randomIntBetween(10, 50); 7 | const res = http.get(`${__ENV.PERSONS_URI}/persons/age/${age}`); 8 | check(res, { 9 | 'is status 200': (res) => res.status === 200, 10 | 'body size is > 0': (r) => r.body.length > 0, 11 | }); 12 | } --------------------------------------------------------------------------------