├── .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 [](https://twitter.com/piotr_minkowski)
2 |
3 | [](https://circleci.com/gh/piomin/sample-spring-boot-on-kubernetes)
4 |
5 | [](https://sonarcloud.io/dashboard?id=piomin_sample-spring-boot-on-kubernetes)
6 | [](https://sonarcloud.io/dashboard?id=piomin_sample-spring-boot-on-kubernetes)
7 | [](https://sonarcloud.io/dashboard?id=piomin_sample-spring-boot-on-kubernetes)
8 | [](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 | }
--------------------------------------------------------------------------------