├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── src ├── main │ ├── resources │ │ └── application.properties │ └── java │ │ └── com │ │ └── example │ │ └── demo │ │ ├── Home.java │ │ └── DemoApplication.java ├── test │ └── java │ │ └── com │ │ └── example │ │ └── demo │ │ └── DemoApplicationTests.java └── k8s │ ├── demo │ ├── kustomization.yaml │ ├── ingress.yaml │ └── deployment.yaml │ └── metrics │ └── manifest.yaml ├── .github ├── workflows │ ├── samples │ │ └── docker-daemon │ │ │ └── simple │ │ │ └── kustomization.yaml │ ├── build.sh │ ├── ci.yml │ └── kind-setup.sh └── act │ ├── Dockerfile │ └── README.md ├── shell.nix ├── default.nix ├── .gitignore ├── Dockerfile ├── skaffold.yaml ├── pom.xml ├── mvnw.cmd ├── mvnw ├── README.md └── notes.md /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsyer/kubernetes-intro/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | info.foo=bar 2 | info.pod=${POD_NAME:-} 3 | management.endpoints.web.exposure.include=* 4 | -------------------------------------------------------------------------------- /.github/workflows/samples/docker-daemon/simple/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namePrefix: fats- 4 | resources: 5 | - ../../../../../src/k8s/demo 6 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /.github/act/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM catthehacker/ubuntu:act-latest 2 | 3 | RUN mkdir -p /etc/nix && echo "build-users-group =" > /etc/nix/nix.conf && \ 4 | curl -L https://nixos.org/nix/install | sh 5 | 6 | ENV USER=root 7 | CMD /bin/bash 8 | ENTRYPOINT ["/bin/bash", "--login", "-c"] -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import { }; 2 | mkShell { 3 | name = "env"; 4 | buildInputs = [ 5 | (import ./default.nix { inherit pkgs; }) 6 | figlet 7 | ]; 8 | shellHook = '' 9 | figlet ":smile:" 10 | kind-setup 11 | kubectl get all 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /.github/act/README.md: -------------------------------------------------------------------------------- 1 | Run Github actions locally using `act`. Build the base image (from the root directory): 2 | 3 | ``` 4 | $ docker build .github/act -t dsyer/act-latest 5 | ``` 6 | 7 | then use it to run the actions: 8 | 9 | ``` 10 | $ act -P ubuntu-latest=dsyer/act-latest -s GITHUB_TOKEN=$GITHUB_TOKEN 11 | ``` -------------------------------------------------------------------------------- /src/test/java/com/example/demo/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class DemoApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/k8s/demo/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | commonLabels: 4 | app: app 5 | images: 6 | - name: apps/demo 7 | newName: localhost:5000/apps/demo 8 | resources: 9 | - deployment.yaml 10 | transformers: 11 | - github.com/dsyer/docker-services/layers/actuator?ref=HEAD 12 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/Home.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | @RestController 7 | public class Home { 8 | @GetMapping("/") 9 | public String home() { 10 | return "Hello World!!"; 11 | } 12 | } -------------------------------------------------------------------------------- /src/k8s/demo/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: ingress 5 | annotations: 6 | kubernetes.io/ingress.class: "nginx" 7 | spec: 8 | rules: 9 | - host: demo 10 | http: 11 | paths: 12 | - path: / 13 | backend: 14 | serviceName: app 15 | servicePort: 80 -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | with pkgs; 4 | 5 | let 6 | 7 | kindSetup = pkgs.writeShellScriptBin "kind-setup" "./.github/workflows/kind-setup.sh"; 8 | 9 | in buildEnv { 10 | name = "env"; 11 | paths = [ 12 | jdk11 13 | apacheHttpd 14 | kind 15 | kubectl 16 | kustomize 17 | skaffold 18 | kindSetup 19 | ]; 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine as build 2 | WORKDIR /workspace/app 3 | 4 | COPY target/*.jar app.jar 5 | 6 | RUN mkdir target && cd target && jar -xf ../*.jar 7 | 8 | FROM openjdk:8-jdk-alpine 9 | VOLUME /tmp 10 | ARG DEPENDENCY=/workspace/app/target 11 | COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /workspace/BOOT-INF/lib 12 | COPY --from=build ${DEPENDENCY}/META-INF /workspace/META-INF 13 | COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /workspace/BOOT-INF/classes 14 | ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -cp /workspace/BOOT-INF/classes:/workspace/BOOT-INF/lib/*:/workspace/BOOT-INF com.example.demo.DemoApplication"] 15 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta10 2 | kind: Config 3 | build: 4 | artifacts: 5 | - image: localhost:5000/apps/demo 6 | # custom: 7 | # buildCommand: ./mvnw spring-boot:build-image -D spring-boot.build-image.imageName=$IMAGE && docker push $IMAGE 8 | buildpacks: 9 | builder: gcr.io/paketo-buildpacks/builder:base 10 | dependencies: 11 | paths: 12 | - pom.xml 13 | - src/main/resources 14 | - target/classes 15 | sync: 16 | manual: 17 | - src: "src/main/resources/**/*" 18 | dest: /workspace/BOOT-INF/classes 19 | strip: src/main/resources/ 20 | - src: "target/classes/**/*" 21 | dest: /workspace/BOOT-INF/classes 22 | strip: target/classes/ 23 | deploy: 24 | kustomize: 25 | paths: 26 | - "src/k8s/demo/" 27 | -------------------------------------------------------------------------------- /.github/workflows/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | export CLUSTER=${CLUSTER-kind} 8 | export CLUSTER_NAME=${CLUSTER_NAME-kind} 9 | export REGISTRY=${REGISTRY-docker-daemon} 10 | export NAMESPACE=${NAMESPACE-default} 11 | 12 | basedir=$(realpath `dirname "${BASH_SOURCE[0]}"`/../..) 13 | cd `dirname "${BASH_SOURCE[0]}"` 14 | 15 | for test in simple; do 16 | echo "##[group]Run kustomize sample $test" 17 | kustomize build samples/${REGISTRY}/${test} 18 | echo "##[endgroup]" 19 | done 20 | 21 | for test in simple; do 22 | echo "##[group]Apply app $test" 23 | kubectl apply \ 24 | -f <(kustomize build samples/${REGISTRY}/${test}) \ 25 | --dry-run --namespace ${NAMESPACE} 26 | echo "##[endgroup]" 27 | done 28 | 29 | #echo "##[group]Skaffold" 30 | #(cd ${basedir}; skaffold run && skaffold delete) 31 | #echo "##[endgroup]" 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | - '!dependabot/**' 8 | pull_request: {} 9 | 10 | jobs: 11 | 12 | fats: 13 | name: FATS 14 | runs-on: ubuntu-latest 15 | env: 16 | CLUSTER: kind 17 | REGISTRY: docker-daemon 18 | DOCKER_BUILDKIT: 1 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: cachix/install-nix-action@v13 22 | with: 23 | nix_path: nixpkgs=channel:nixos-unstable 24 | - name: Setup env 25 | run: | 26 | . ~/.profile 27 | ID=$(date +%s) # TODO use something that is assigned by CI to guarantee uniqueness 28 | echo "JOB_ID=${ID}" 29 | echo "CLUSTER_NAME=cli-${ID}" >> $GITHUB_ENV 30 | echo "NAMESPACE=cli-${ID}" >> $GITHUB_ENV 31 | nix-env -i -f default.nix 32 | ./.github/workflows/kind-setup.sh 33 | shell: bash 34 | - name: Build Apps 35 | run: .github/workflows/build.sh 36 | shell: bash 37 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.actuate.health.Health; 5 | import org.springframework.boot.actuate.health.HealthIndicator; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @SpringBootApplication(proxyBeanMethods = false) 11 | public class DemoApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(DemoApplication.class, args); 15 | } 16 | 17 | } 18 | 19 | @RestController 20 | class JunkHealthIndicator implements HealthIndicator { 21 | 22 | private static Health OUT_OF_SERVICE = Health.outOfService().build(); 23 | private static Health OK = Health.up().build(); 24 | 25 | private Health status = OK; 26 | 27 | @Override 28 | public Health health() { 29 | return status; 30 | } 31 | 32 | @PostMapping("/die") 33 | public String die() { 34 | status = OUT_OF_SERVICE; 35 | return "Switched to OUT_OF_SERVICE"; 36 | } 37 | 38 | @PostMapping("/live") 39 | public String live() { 40 | status = OK; 41 | return "Switched to OK"; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/k8s/demo/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: app 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | containers: 10 | - image: apps/demo 11 | name: app 12 | env: 13 | - name: MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE 14 | value: "*" 15 | - name: JAVA_TOOL_OPTIONS 16 | value: -Dspring.devtools.restart.enabled=true 17 | - name: POD_NAME 18 | valueFrom: 19 | fieldRef: 20 | fieldPath: metadata.name 21 | resources: 22 | requests: 23 | cpu: 500m 24 | limits: 25 | cpu: 4000m 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: app 31 | spec: 32 | ports: 33 | - name: 80-8080 34 | port: 80 35 | protocol: TCP 36 | targetPort: 8080 37 | 38 | --- 39 | apiVersion: autoscaling/v2beta2 40 | kind: HorizontalPodAutoscaler 41 | metadata: 42 | name: app 43 | spec: 44 | maxReplicas: 3 45 | metrics: 46 | - resource: 47 | name: cpu 48 | target: 49 | averageUtilization: 10 50 | type: Utilization 51 | type: Resource 52 | minReplicas: 1 53 | scaleTargetRef: 54 | apiVersion: apps/v1 55 | kind: Deployment 56 | name: app 57 | -------------------------------------------------------------------------------- /.github/workflows/kind-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=${KIND_VERSION:-v1.19.0} 4 | clusters=$(kind get clusters) 5 | reg_name='registry' 6 | reg_port='5000' 7 | 8 | # desired cluster name; default is "kind" 9 | KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-kind}" 10 | 11 | function start_registry() { 12 | # create registry container unless it already exists 13 | running=$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true) 14 | if [ "${running}" != 'true' ]; then 15 | docker run \ 16 | -d --restart=always -p "${reg_port}:5000" --name "${reg_name}" \ 17 | registry:2 18 | fi 19 | } 20 | 21 | function create_cluster() { 22 | version=$1 23 | reg_ip=$(docker inspect -f '{{.NetworkSettings.IPAddress}}' "${reg_name}") 24 | 25 | # create a cluster with the local registry enabled in containerd 26 | cat < ~/.kube/kind-config-internal 53 | kind get kubeconfig > ~/.kube/kind 54 | KUBECONFIG=~/.kube/kind:~/.kube/config kubectl config view --merge --flatten > .config.yaml 55 | mv .config.yaml ~/.kube/config 56 | 57 | # Document the local registry 58 | # https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry 59 | cat < 2 | 3 | 4.0.0 4 | 5 | org.springframework.boot 6 | spring-boot-starter-parent 7 | 2.4.2 8 | 9 | 10 | com.example 11 | demo 12 | 0.0.1-SNAPSHOT 13 | demo 14 | Demo project for Spring Boot 15 | 16 | 1.8 17 | localhost:5000/apps/${project.artifactId} 18 | false 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-actuator 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-webflux 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-test 32 | test 33 | 34 | 35 | org.junit.vintage 36 | junit-vintage-engine 37 | 38 | 39 | 40 | 41 | io.projectreactor 42 | reactor-test 43 | test 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-devtools 48 | runtime 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-maven-plugin 56 | 57 | 58 | 59 | 60 | 61 | spring-milestones 62 | Spring Milestones 63 | https://repo.spring.io/milestone 64 | 65 | 66 | 67 | 68 | spring-milestones 69 | Spring Milestones 70 | https://repo.spring.io/milestone 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/k8s/metrics/manifest.yaml: -------------------------------------------------------------------------------- 1 | # Use this to install the metrics-server in a Kind cluster 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | name: system:aggregated-metrics-reader 7 | labels: 8 | rbac.authorization.k8s.io/aggregate-to-view: "true" 9 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 10 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 11 | rules: 12 | - apiGroups: ["metrics.k8s.io"] 13 | resources: ["pods", "nodes"] 14 | verbs: ["get", "list", "watch"] 15 | --- 16 | apiVersion: rbac.authorization.k8s.io/v1 17 | kind: ClusterRoleBinding 18 | metadata: 19 | name: metrics-server:system:auth-delegator 20 | roleRef: 21 | apiGroup: rbac.authorization.k8s.io 22 | kind: ClusterRole 23 | name: system:auth-delegator 24 | subjects: 25 | - kind: ServiceAccount 26 | name: metrics-server 27 | namespace: kube-system 28 | --- 29 | apiVersion: rbac.authorization.k8s.io/v1 30 | kind: RoleBinding 31 | metadata: 32 | name: metrics-server-auth-reader 33 | namespace: kube-system 34 | roleRef: 35 | apiGroup: rbac.authorization.k8s.io 36 | kind: Role 37 | name: extension-apiserver-authentication-reader 38 | subjects: 39 | - kind: ServiceAccount 40 | name: metrics-server 41 | namespace: kube-system 42 | --- 43 | apiVersion: apiregistration.k8s.io/v1beta1 44 | kind: APIService 45 | metadata: 46 | name: v1beta1.metrics.k8s.io 47 | spec: 48 | service: 49 | name: metrics-server 50 | namespace: kube-system 51 | group: metrics.k8s.io 52 | version: v1beta1 53 | insecureSkipTLSVerify: true 54 | groupPriorityMinimum: 100 55 | versionPriority: 100 56 | --- 57 | apiVersion: v1 58 | kind: ServiceAccount 59 | metadata: 60 | name: metrics-server 61 | namespace: kube-system 62 | --- 63 | apiVersion: apps/v1 64 | kind: Deployment 65 | metadata: 66 | name: metrics-server 67 | namespace: kube-system 68 | labels: 69 | k8s-app: metrics-server 70 | spec: 71 | selector: 72 | matchLabels: 73 | k8s-app: metrics-server 74 | template: 75 | metadata: 76 | name: metrics-server 77 | labels: 78 | k8s-app: metrics-server 79 | spec: 80 | serviceAccountName: metrics-server 81 | volumes: 82 | # mount in tmp so we can safely use from-scratch images and/or read-only containers 83 | - name: tmp-dir 84 | emptyDir: {} 85 | containers: 86 | - name: metrics-server 87 | image: k8s.gcr.io/metrics-server/metrics-server:v0.3.7 88 | imagePullPolicy: IfNotPresent 89 | args: 90 | - --cert-dir=/tmp 91 | - --secure-port=4443 92 | - --kubelet-insecure-tls 93 | - --kubelet-preferred-address-types=InternalIP 94 | - /metrics-server 95 | - --v=2 96 | ports: 97 | - name: main-port 98 | containerPort: 4443 99 | protocol: TCP 100 | securityContext: 101 | readOnlyRootFilesystem: true 102 | runAsNonRoot: true 103 | runAsUser: 1000 104 | volumeMounts: 105 | - name: tmp-dir 106 | mountPath: /tmp 107 | nodeSelector: 108 | kubernetes.io/os: linux 109 | --- 110 | apiVersion: v1 111 | kind: Service 112 | metadata: 113 | name: metrics-server 114 | namespace: kube-system 115 | labels: 116 | kubernetes.io/name: "Metrics-server" 117 | kubernetes.io/cluster-service: "true" 118 | spec: 119 | selector: 120 | k8s-app: metrics-server 121 | ports: 122 | - port: 443 123 | protocol: TCP 124 | targetPort: main-port 125 | --- 126 | apiVersion: rbac.authorization.k8s.io/v1 127 | kind: ClusterRole 128 | metadata: 129 | name: system:metrics-server 130 | rules: 131 | - apiGroups: 132 | - "" 133 | resources: 134 | - pods 135 | - nodes 136 | - nodes/stats 137 | - namespaces 138 | - configmaps 139 | verbs: 140 | - get 141 | - list 142 | - watch 143 | --- 144 | apiVersion: rbac.authorization.k8s.io/v1 145 | kind: ClusterRoleBinding 146 | metadata: 147 | name: system:metrics-server 148 | roleRef: 149 | apiGroup: rbac.authorization.k8s.io 150 | kind: ClusterRole 151 | name: system:metrics-server 152 | subjects: 153 | - kind: ServiceAccount 154 | name: metrics-server 155 | namespace: kube-system 156 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | * [Pre-requisites](#pre-requisites) 2 | * [Getting Started](#getting-started) 3 | * [Deploy to Kubernetes](#deploy-to-kubernetes) 4 | * [Organize with Kustomize](#organize-with-kustomize) 5 | * [Modularize](#modularize) 6 | * [Developer Experience with Skaffold](#developer-experience-with-skaffold) 7 | * [Spring Boot Features](#spring-boot-features) 8 | * [Buildpack Images](#buildpack-images) 9 | * [Using Spring Boot Docker Images with Skaffold](#using-spring-boot-docker-images-with-skaffold) 10 | * [Hot Reload in Skaffold with Spring Boot Devtools](#hot-reload-in-skaffold-with-spring-boot-devtools) 11 | * [Layered JARs](#layered-jars) 12 | * [Probes](#probes) 13 | * [Graceful Shutdown](#graceful-shutdown) 14 | * [The Bad Bits: Ingress](#the-bad-bits-ingress) 15 | * [The Bad Bits: Persistent Volumes](#the-bad-bits-persistent-volumes) 16 | * [The Bad Bits: Secrets](#the-bad-bits-secrets) 17 | * [A Different Approach to Boilerplate YAML](#a-different-approach-to-boilerplate-yaml) 18 | * [Another Idea](#another-idea) 19 | * [Metrics Server](#metrics-server) 20 | * [Autoscaler](#autoscaler) 21 | 22 | ## Pre-requisites 23 | 24 | You need Docker. If you can install [Nix](https://nixos.org/nix/) then do that and then just `nix-shell` on your command line to install all dependencies except Docker. If you can't do that, you will need to install them manually. Here's what it installs: 25 | 26 | - `jdk11` 27 | - `kind` (you might not need that if you can get hold of a Kubernetes cluster some other way) 28 | - `kubectl` 29 | - `kustomize` 30 | - `skaffold` 31 | - `apacheHttpd` just to get the `ab` utility for load generation 32 | 33 | There is also `kind-setup.sh` script that you might feel like using to set up a Kubernetes cluster and a Docker registry (`nix-shell` will run it automatically). Maybe an IDE would come in handy, but not mandatory. 34 | 35 | ## Getting Started 36 | 37 | Create a basic Spring Boot application: 38 | 39 | ``` 40 | $ curl https://start.spring.io/starter.tgz -d dependencies=webflux -d dependencies=actuator | tar -xzvf - 41 | ``` 42 | 43 | Add an endpoint (`src/main/java/com/example/demo/Home.java`): 44 | 45 | ```java 46 | package com.example.demo; 47 | 48 | import org.springframework.web.bind.annotation.GetMapping; 49 | import org.springframework.web.bind.annotation.RestController; 50 | 51 | @RestController 52 | public class Home { 53 | @GetMapping("/") 54 | public String home() { 55 | return "Hello World"; 56 | } 57 | } 58 | ``` 59 | 60 | Containerize (`Dockerfile`): 61 | 62 | ``` 63 | FROM openjdk:8-jdk-alpine as build 64 | WORKDIR /workspace/app 65 | 66 | COPY target/*.jar app.jar 67 | 68 | RUN mkdir target && cd target && jar -xf ../*.jar 69 | 70 | FROM openjdk:8-jdk-alpine 71 | VOLUME /tmp 72 | ARG DEPENDENCY=/workspace/app/target 73 | COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib 74 | COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF 75 | COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app 76 | ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.demo.DemoApplication"] 77 | ``` 78 | 79 | Run and test... 80 | 81 | ``` 82 | $ ./mvnw package 83 | $ docker build -t localhost:5000/apps/demo . 84 | $ docker run -p 8080:8080 localhost:5000/apps/demo 85 | $ curl localhost:8080 86 | Hello World 87 | ``` 88 | 89 | Stash the image for later in our local repository (which was started with `kind-setup` if you used that): 90 | 91 | ``` 92 | $ docker push localhost:5000/apps/demo 93 | ``` 94 | 95 | ## Deploy to Kubernetes 96 | 97 | Create a basic manifest: 98 | 99 | ``` 100 | $ kubectl create deployment demo --image=localhost:5000/apps/demo --dry-run -o=yaml > deployment.yaml 101 | $ echo --- >> deployment.yaml 102 | $ kubectl create service clusterip demo --tcp=80:8080 --dry-run -o=yaml >> deployment.yaml 103 | ``` 104 | 105 | Apply it: 106 | 107 | ``` 108 | $ kubectl apply -f deployment.yaml 109 | $ kubectl port-forward svc/demo 8080:80 110 | $ curl localhost:8080 111 | Hello World 112 | ``` 113 | 114 | ## Organize with Kustomize 115 | 116 | ``` 117 | $ mkdir -p k8s 118 | $ mv deployment.yaml k8s 119 | ``` 120 | 121 | Create `k8s/kustomization.yaml`: 122 | 123 | ```yaml 124 | apiVersion: kustomize.config.k8s.io/v1beta1 125 | kind: Kustomization 126 | resources: 127 | - deployment.yaml 128 | ``` 129 | 130 | Apply the new manifest (which is so far just the same): 131 | 132 | ``` 133 | $ kubectl delete -f k8s/deployment.yaml 134 | $ kubectl apply -k k8s/ 135 | service/demo created 136 | deployment.apps/demo created 137 | ``` 138 | 139 | Now we can strip away some of the manifest and let Kustomize fill in the gaps (`deployment.yaml`): 140 | 141 | ```yaml 142 | apiVersion: apps/v1 143 | kind: Deployment 144 | metadata: 145 | name: demo 146 | spec: 147 | template: 148 | spec: 149 | containers: 150 | - image: localhost:5000/apps/demo 151 | name: demo 152 | --- 153 | apiVersion: v1 154 | kind: Service 155 | metadata: 156 | name: demo 157 | spec: 158 | ports: 159 | - name: 80-8080 160 | port: 80 161 | protocol: TCP 162 | targetPort: 8080 163 | ``` 164 | 165 | Add labels to the kustomization: 166 | 167 | ```yaml 168 | apiVersion: kustomize.config.k8s.io/v1beta1 169 | kind: Kustomization 170 | commonLabels: 171 | app: app 172 | resources: 173 | - deployment.yaml 174 | ``` 175 | 176 | Maybe switch to `kustomize` on the command line (to pick up latest version, although at this stage it doesn't matter): 177 | 178 | ``` 179 | $ kubectl apply -f <(kustomize build k8s) 180 | ``` 181 | 182 | ## Modularize 183 | 184 | Delete the current deployment: 185 | 186 | ``` 187 | $ kubectl delete -f k8s/deployment.yaml 188 | ``` 189 | 190 | and then remove `deployment.yaml` and replace the reference to it in the kustomization with an example from a library, adding also an image replacement: 191 | 192 | ```yaml 193 | apiVersion: kustomize.config.k8s.io/v1beta1 194 | kind: Kustomization 195 | commonLabels: 196 | app: app 197 | images: 198 | - name: dsyer/template 199 | newName: localhost:5000/apps/demo 200 | resources: 201 | - github.com/dsyer/docker-services/layers/base 202 | ``` 203 | 204 | Deploy again: 205 | 206 | ``` 207 | $ kubectl apply -f <(kustomize build k8s/) 208 | configmap/env-config created 209 | service/app created 210 | deployment.apps/app created 211 | ``` 212 | 213 | You can also add features from the library as patches. E.g. tell Kubernetes that we have Spring Boot actuators in our app: 214 | 215 | ```yaml 216 | apiVersion: kustomize.config.k8s.io/v1beta1 217 | kind: Kustomization 218 | ... 219 | transformers: 220 | - github.com/dsyer/docker-services/layers/actuator 221 | ``` 222 | 223 | Deploy it: 224 | 225 | ``` 226 | $ kubectl apply -f <(kustomize build k8s/) 227 | configmap/env-config unchanged 228 | service/app unchanged 229 | deployment.apps/app configured 230 | ``` 231 | 232 | Something changed in the deployment (liveness and readiness probes): 233 | 234 | ```yaml 235 | apiVersion: apps/v1 236 | kind: Deployment 237 | --- 238 | livenessProbe: 239 | httpGet: 240 | path: /actuator/info 241 | port: 8080 242 | initialDelaySeconds: 10 243 | periodSeconds: 3 244 | name: app 245 | readinessProbe: 246 | httpGet: 247 | path: /actuator/health 248 | port: 8080 249 | initialDelaySeconds: 20 250 | periodSeconds: 10 251 | ``` 252 | 253 | ## Developer Experience with Skaffold 254 | 255 | [Skaffold](https://skaffold.dev) is a tool from Google that helps reduce toil for the change-build-test cycle including deploying to Kubernetes. We can start with a really simple Docker based build (in `skaffold.yaml`): 256 | 257 | ```yaml 258 | apiVersion: skaffold/v2beta10 259 | kind: Config 260 | build: 261 | artifacts: 262 | - image: localhost:5000/apps/demo 263 | docker: {} 264 | deploy: 265 | kustomize: 266 | paths: 267 | - k8s 268 | ``` 269 | 270 | Start the app: 271 | 272 | ``` 273 | $ skaffold dev --port-forward 274 | ... 275 | Watching for changes... 276 | Port forwarding service/app in namespace default, remote port 80 -> address 127.0.0.1 port 4503 277 | ... 278 | ``` 279 | 280 | You can test that the app is running on port 4503. Because of the way we defined our `Dockerfile`, it is watching for changes in the jar file. So we can make as many changes as we want to the source code and they only get deployed if we rebuild the jar. 281 | 282 | ## Spring Boot Features 283 | 284 | - Loading `application.properties` and `application.yml` 285 | - Autoconfiguration of databases, message brokers, etc. 286 | - Decryption of encrypted secrets in process (e.g. Spring Cloud Commons and Spring Cloud Vault) 287 | - Spring Cloud Kubernetes (direct access to Kubernetes API required for some features) 288 | - \+ Buildpack support in `pom.xml` or `build.gradle` 289 | - Actuators (separate port or not?) 290 | - \+ Liveness and Readiness as first class features 291 | - \+ Graceful shutdown 292 | - ? Support for actuators with Kubernetes API keys 293 | 294 | ## Buildpack Images 295 | 296 | To get a buildpack image, ensure you are using Spring Boot at least 2.3 (`pom.xml`): 297 | 298 | ```xml 299 | 300 | 302 | 4.0.0 303 | 304 | org.springframework.boot 305 | spring-boot-starter-parent 306 | 2.4.2 307 | 308 | 309 | 310 | ``` 311 | 312 | and run the plugin on the command line: 313 | 314 | ``` 315 | $ rm Dockerfile 316 | $ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=localhost:5000/apps/demo 317 | ... 318 | [INFO] Successfully built image 'docker.io/library/demo:0.0.1-SNAPSHOT' 319 | [INFO] 320 | [INFO] ------------------------------------------------------------------------ 321 | [INFO] BUILD SUCCESS 322 | [INFO] ------------------------------------------------------------------------ 323 | ... 324 | $ docker run -p 8080:8080 demo:0.0.1-SNAPSHOT localhost:5000/apps/demo 325 | Container memory limit unset. Configuring JVM for 1G container. 326 | Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=86381K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx450194K (Head Room: 0%, Loaded Class Count: 12837, Thread Count: 250, Total Memory: 1073741824) 327 | ... 328 | ``` 329 | 330 | > NOTE: The CF memory calculator is used at runtime to size the JVM to fit the container. 331 | 332 | You can also change the image tag (in `pom.xml` or on the command line with `-D`): 333 | 334 | ```xml 335 | 336 | ... 337 | 338 | 1.8 339 | localhost:5000/apps/${project.artifactId} 340 | 341 | 342 | ``` 343 | 344 | ## Using Spring Boot Docker Images with Skaffold 345 | 346 | If you use Skaffold 1.11.0 or better you can use the `buildpacks` builder: 347 | 348 | ```yaml 349 | apiVersion: skaffold/v2beta10 350 | kind: Config 351 | build: 352 | artifacts: 353 | - image: localhost:5000/apps/demo 354 | buildpacks: 355 | builder: gcr.io/paketo-buildpacks/builder:base 356 | dependencies: 357 | paths: 358 | - pom.xml 359 | - src/main/resources 360 | - target/classes 361 | sync: 362 | manual: 363 | - src: "src/main/resources/**/*" 364 | dest: /workspace/BOOT-INF/classes 365 | strip: src/main/resources/ 366 | - src: "target/classes/**/*" 367 | dest: /workspace/BOOT-INF/classes 368 | strip: target/classes/ 369 | deploy: 370 | kustomize: 371 | paths: 372 | - "src/k8s/demo/" 373 | ``` 374 | 375 | Skaffold also has a custom builder option, so we can use that to do the same thing effectively: 376 | 377 | ```yaml 378 | apiVersion: skaffold/v2beta10 379 | kind: Config 380 | build: 381 | artifacts: 382 | - image: localhost:5000/apps/demo 383 | custom: 384 | buildCommand: ./mvnw spring-boot:build-image -D spring-boot.build-image.imageName=$IMAGE && docker push $IMAGE 385 | ... 386 | ``` 387 | 388 | ## Hot Reload in Skaffold with Spring Boot Devtools 389 | 390 | Add `spring-boot-devtools` to your project `pom.xml`: 391 | 392 | ```xml 393 | 394 | org.springframework.boot 395 | spring-boot-devtools 396 | runtime 397 | 398 | ``` 399 | 400 | and make sure it gets added to the runtime image in (see `excludeDevtools`): 401 | 402 | ```xml 403 | 404 | false 405 | 406 | ``` 407 | 408 | Then in `skaffold.yaml` we can use changes in source files to sync to the running container instead of doing a full rebuild. 409 | The key parts of this are the `custom.dependencies` and `sync.manual` fields. They have to match - i.e. no files are copied into the running container from `sync` if they don't appear also in `dependencies`. The effect is that if any `.java` or `.properties` files are changed, they are copied into the running container, and this causes Spring Boot to restart the app, usually quite quickly. 410 | 411 | > NOTE: You can use Skaffold and Maven "profiles" to keep the devtools stuff only at dev time. The production image can be built without the devtools dependency if the flag is inverted or the dependency is removed. 412 | 413 | You need to tell Spring Boot that it should run with devtools reloading, even though it is running from the `JarLauncher`. You can do that by setting an environment variable in the pod container: `JAVA_TOOL_OPTIONS` which is appended to the JVM launch args (the value is `-Dspring.devtools.restart.enabled=true`). Environment variables can be set in `deployment.yaml`: 414 | 415 | ```yaml 416 | ... 417 | spec: 418 | replicas: 1 419 | template: 420 | spec: 421 | containers: 422 | - image: apps/demo 423 | name: app 424 | - name: JAVA_TOOL_OPTIONS 425 | value: -Dspring.devtools.restart.enabled=true 426 | ... 427 | ``` 428 | 429 | or via the buildpack (in `pom.xml`): 430 | 431 | ``` 432 | 433 | 434 | 435 | org.springframework.boot 436 | spring-boot-maven-plugin 437 | 438 | 439 | 440 | -Dspring.devtools.restart.enabled=true 441 | 442 | 443 | 444 | false 445 | 446 | 447 | 448 | 449 | ``` 450 | 451 | ## Layered JARs 452 | 453 | Spring Boot 2.3 also has some [capabilities](https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/maven-plugin/reference/html/#repackage) to unpack its executable jars in a way that can easily be mapped to more finely grained filesystem layers in a container. You have to switch on the layering feature in the build (in `pom.xml`): 454 | 455 | ``` 456 | 457 | org.springframework.boot 458 | spring-boot-maven-plugin 459 | 460 | 461 | true 462 | 463 | ... 464 | 465 | 466 | ``` 467 | 468 | By default it splits a JAR into 4 layers and you can list and extract them by using the JAR file itself and a system property: 469 | 470 | ``` 471 | $ java -jar -Djarmode=layertools target/docker-demo-0.0.1-SNAPSHOT.jar list 472 | dependencies 473 | spring-boot-loader 474 | snapshot-dependencies 475 | application 476 | ``` 477 | 478 | > NOTE: It actually doesn't add much value over the standard JAR layout unless you have snapshot dependencies. 479 | 480 | Then if you ask it to `extract` (instead of `list`) it will dump the contents of those layers in the current directory. Here's a `Dockerfile` that works with that: 481 | 482 | ``` 483 | # syntax=docker/dockerfile:experimental 484 | FROM openjdk:8-jdk-alpine as build 485 | WORKDIR /workspace/app 486 | 487 | COPY mvnw . 488 | COPY .mvn .mvn 489 | COPY pom.xml . 490 | COPY src src 491 | 492 | RUN --mount=type=cache,target=/root/.m2 ./mvnw install -DskipTests 493 | RUN mkdir -p target/dependency && (cd target/dependency; java -Djarmode=layertools -jar ../*.jar extract) 494 | 495 | FROM openjdk:8-jre-alpine 496 | RUN addgroup -S demo && adduser -S demo -G demo 497 | VOLUME /tmp 498 | ARG DEPENDENCY=/workspace/app/target/dependency 499 | COPY --from=build ${DEPENDENCY}/dependencies/BOOT-INF/lib /app/lib 500 | COPY --from=build ${DEPENDENCY}/application/META-INF /app/META-INF 501 | COPY --from=build ${DEPENDENCY}/application/BOOT-INF/classes /app 502 | RUN chown -R demo:demo /app 503 | USER demo 504 | ENTRYPOINT ["sh", "-c", "java -cp /app:/app/lib/* com.example.demo.DemoApplication ${0} ${@}"] 505 | ``` 506 | 507 | If you want to get fancy you can tweak the layer definitions to make new layers and put whatever files you like in each layer. 508 | 509 | ## Probes 510 | 511 | Kubernetes uses two probes to determine if the app is ready to accept traffic and whether the app is alive: 512 | 513 | * If the readiness probe does not return a `200` no trafic will be routed to it 514 | * If the liveness probe does not return a `200` kubernetes will restart the Pod 515 | 516 | Spring Boot has a build in set of endpoints from the [Actuator](https://spring.io/blog/2020/03/25/liveness-and-readiness-probes-with-spring-boot) module that fit nicely into these use cases 517 | 518 | * The `/health/readiness` endpoint indicates if the application is healthy, this fits with the readiness proble 519 | * The `/health/liveness` endpoint serves application info, we can use this to make sure the application is "alive" 520 | 521 | > NOTE: before Spring Boot 2.3, `/health` and `/info` worked just as well for most apps. The new endpoints are more flexible and configurable for specific use cases. 522 | 523 | Here's a basic patch that works (`k8s/probes.yaml`): 524 | 525 | ```yaml 526 | apiVersion: apps/v1 527 | kind: Deployment 528 | metadata: 529 | name: app 530 | spec: 531 | template: 532 | spec: 533 | containers: 534 | - name: app 535 | readinessProbe: 536 | httpGet: 537 | port: 8080 538 | path: /actuator/health/readiness 539 | livenessProbe: 540 | httpGet: 541 | port: 8080 542 | path: /actuator/health/liveness 543 | ``` 544 | 545 | You can install it in the `kustomization.yaml`: 546 | 547 | ```yaml 548 | apiVersion: kustomize.config.k8s.io/v1beta1 549 | kind: Kustomization 550 | commonLabels: 551 | app: demo 552 | images: 553 | - name: dsyer/template 554 | newName: localhost:5000/apps/demo 555 | resources: 556 | - github.com/dsyer/docker-services/layers/base 557 | patchesStrategicMerge: 558 | - probes.yaml 559 | ``` 560 | 561 | ## Graceful Shutdown 562 | 563 | Kubernetes sends `SIGTERM` to containers it wants to shutdown. It tries to shield them from further traffic, but there are no guarantees in a distributed system. It then waits for a grace period (by default 30s) before shooting the container in the head. 564 | 565 | If your app has a lot of traffic, or takes a long time to process requests, you may have to take steps to avoid lost connections. One thing you can always do is add a sleep to the Kubernetes `preStop` hook: 566 | 567 | ```yaml 568 | apiVersion: apps/v1 569 | kind: Deployment 570 | metadata: 571 | name: app 572 | spec: 573 | template: 574 | spec: 575 | containers: 576 | - name: app 577 | lifecycle: 578 | preStop: 579 | exec: 580 | command: ["sh", "-c", "sleep 10"] 581 | ``` 582 | 583 | Also, it is possible that the `ApplicationContext` could close before the HTTP server shuts down, so in-flight requests can fail because they no longer have access to database connections etc. Spring Boot 2.3 has some new configuration properties 584 | 585 | ``` 586 | server.shutdown=graceful 587 | spring.lifecycle.timeout-per-shutdown-phase=20s 588 | ``` 589 | 590 | to delay the hard stop of the HTTP server while the `ApplicationContext` is still running. 591 | 592 | ## The Bad Bits: Ingress 593 | 594 | > NOTE: If your cluster is "brand new" and doesn't have an ingress service you can add one like this: 595 | > 596 | > $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/mandatory.yaml 597 | > $ kubectl apply -f <(curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/provider/baremetal/service-nodeport.yaml | sed -e '/ type:.*/d') 598 | 599 | [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) in Kubernetes refers to an API resource that defines how HTTP requests get routed to applications (or rather services). You can create rules based on hostname or URL paths. Example: 600 | 601 | ```yaml 602 | apiVersion: networking.k8s.io/v1beta1 603 | kind: Ingress 604 | metadata: 605 | name: ingress 606 | annotations: 607 | kubernetes.io/ingress.class: "nginx" 608 | spec: 609 | rules: 610 | - host: demo 611 | http: 612 | paths: 613 | - path: / 614 | backend: 615 | serviceName: app 616 | servicePort: 80 617 | ``` 618 | 619 | Apply this YAML and check the status: 620 | 621 | ``` 622 | $ kubectl apply -f k8s/ingress.yaml 623 | $ kubectl get ingress 624 | NAME HOSTS ADDRESS PORTS AGE 625 | ingress demo 10.103.4.16 80 2m15s 626 | ``` 627 | 628 | So it's working (because we had nginx ingress already installed in the cluster) and we can connect to the service through the ingress: 629 | 630 | ``` 631 | $ kubectl port-forward --namespace=ingress-nginx service/ingress-nginx 8080:80 632 | $ curl localhost:8080 -H "Host: demo" 633 | Hello World!! 634 | ``` 635 | 636 | Having to add `Host:demo` to HTTP requests manually is kind of a pain. Normally you want to rely on default behaviour of HTTP clients (like browsers) and just `curl demo`. But that means you need DNS or `/etc/hosts` configuration and that's where it gets to be even more painful. DNS changes can take minutes or even hours to propagate, and `/etc/hosts` only works on your machine. 637 | 638 | ## The Bad Bits: Persistent Volumes 639 | 640 | How about an app with a database? Let's look at a PetClinic: 641 | 642 | ``` 643 | $ kubectl apply -f <(kustomize build github.com/dsyer/docker-services/layers/samples/petclinic) 644 | configmap/petclinic-env-config created 645 | configmap/petclinic-mysql-config created 646 | configmap/petclinic-mysql-env created 647 | service/petclinic-app created 648 | service/petclinic-mysql created 649 | deployment.apps/petclinic-app created 650 | deployment.apps/petclinic-mysql created 651 | persistentvolumeclaim/petclinic-mysql created 652 | $ kubectl port-forward service/petclinic-app 8080:80 653 | ``` 654 | 655 | Visit `http://localhost:8080` in your browser: 656 | 657 | ![PetClinic](https://i.imgur.com/MCgAL4n.png) 658 | 659 | So that works. What's the problem? 660 | 661 | ``` 662 | $ kubectl get persistentvolumeclaim 663 | NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE 664 | petclinic-mysql Bound pvc-68d43a16-1953-4754-893c-5f383556b912 8Gi RWO standard 5m25s 665 | ``` 666 | 667 | All perfectly fine, so what is the problem? The PVC is "Bound", which means there was a PV that satisfied its resource constraints. That won't always be the case, and you might need an admin operator to fix it. It worked here because there is a default PV. Some platforms have that, and some don't. 668 | 669 | More issues with this PetClinic: 670 | 671 | - The database is tied to the app, all wrapped up in the same manifest. It's great for getting started and getting something running, but it won't be structured like that in production. 672 | 673 | - The database probably isn't fit for production use. It has a clear text password for instance. 674 | 675 | ## The Bad Bits: Secrets 676 | 677 | Kubernetes has a [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) feature built in, but everyone knows they are not encrypted, so not really secret. There are several approaches that work to encrypt secrets, but it's not standardized and it's hard to operationalize. We've been here before, so Vault and/or Credhub will probably end up being a solution. 678 | 679 | For high-end, super security-concious sites, you have to ensure that unencrypted data is never at rest, anywhere (including in an ephemeral file system in a container volume). Applications have to be able to decrypt secrets in-process, so Spring can help with that, but in practice only if users explicitly ask for it. 680 | 681 | ## A Different Approach to Boilerplate YAML 682 | 683 | Kubernetes is flexible and extensible. How about a CRD (Custom Resource Definition)? E.g. what if our demo manifest was just this: 684 | 685 | ```yaml 686 | apiVersion: spring.io/v1 687 | kind: Microservice 688 | metadata: 689 | name: demo 690 | spec: 691 | image: localhost:5000/apps/demo 692 | ``` 693 | 694 | That's actually all you need to know to create the 30-50 lines of YAML in the original sample. The idea is to expand it in cluster and create the service, deployment (ingress, etc.) automatically. Kubernetes also ties those resources to the "microservice" and does garbage collection - if you delete the parent resource they all get deleted. There are other benefits to having an abstraction that is visible in the Kubernetes API. 695 | 696 | This needs to be wired into the Kubernetes cluster. There's a prototype [here](https://github.com/dsyer/spring-boot-operator) and `Microservice` is also pretty similar to the [projectriff](https://github.com/projectriff/riff) resource called `Deployer`. Other implementations have also been seen on the internet. 697 | 698 | The prototype has a [PetClinic](https://github.com/dsyer/spring-boot-operator/blob/master/config/samples/petclinic.yaml) that you can deploy to get a feeling for the differences. Here's the manifest: 699 | 700 | ```yaml 701 | apiVersion: spring.io/v1 702 | kind: Microservice 703 | metadata: 704 | name: petclinic 705 | spec: 706 | image: dsyer/petclinic 707 | bindings: 708 | - services/mysql 709 | template: 710 | spec: 711 | containers: 712 | - name: app 713 | env: 714 | - name: MANAGEMENT_ENDPOINTS_WEB_BASEPATH 715 | value: /actuator 716 | - name: DATABASE 717 | value: mysql 718 | ``` 719 | 720 | The danger with such abstractions is that they potentially close off areas that were formally verbose but flexible. Also, there is a problem with cognitive-saturation - too many CRDs means too many things to learn and too many to keep track of in your cluster. 721 | 722 | ## Another Idea 723 | 724 | Instead of a CRD that creates deployments, you could have a CRD that injects stuff into existing deployments. Kubernetes has a `PodPreset` feature that is a similar idea, and the implementation would be similar (a mutating webhook). E.g. 725 | 726 | ```yaml 727 | apiVersion: spring.io/v1 728 | kind: Microservice 729 | metadata: 730 | name: demo 731 | spec: 732 | target: 733 | apiVersion: apps/v1 734 | kind: Deployment 735 | name: demo 736 | ``` 737 | 738 | The "opinions" about what to inject into the "demo" deployment could be located in the Microservice controller. It can look at the metadata in the container image and add probes that match the dependencies - if actuator is present use `/actuator/info`. If there is no metadata in the container the manifest can list opinions explicitly. 739 | 740 | The "target" for the Microservice could also be a Kubernetes selector (e.g. all deployments with a specific label). 741 | 742 | ## Metrics Server 743 | 744 | You need a [Metrics Server](https://github.com/kubernetes-sigs/metrics-server) to benefit from `kubectl top` and the [Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/). Kind doesn't support the metrics server [out of the box](https://github.com/kubernetes-sigs/kind/issues/398): 745 | 746 | ``` 747 | $ kubectl top pod 748 | W0323 08:01:25.173488 18448 top_pod.go:266] Metrics not available for pod default/app-5f969c594d-79s79, age: 65h4m54.173475197s 749 | error: Metrics not available for pod default/app-5f969c594d-79s79, age: 65h4m54.173475197s 750 | ``` 751 | 752 | But you _can_ install it using the manifests in the [source code](https://github.com/kubernetes-sigs/metrics-server/blob/master/deploy/kubernetes/). You might need to tweak the deployment of the `metrics-server` to allow it to access the k8s API. A manifest is available here: 753 | 754 | ``` 755 | $ kubectl apply -f src/k8s/metrics 756 | $ kubectl top pod 757 | NAME CPU(cores) MEMORY(bytes) 758 | app-79fdc46f88-mjm5c 217m 143Mi 759 | ``` 760 | 761 | > NOTE: You might need to recycle the application Pods to make them wake up to the metrics server. 762 | 763 | ## Autoscaler 764 | 765 | First make sure you have a CPU request in your app container: 766 | 767 | ```yaml 768 | apiVersion: apps/v1 769 | kind: Deployment 770 | metadata: 771 | name: app 772 | spec: 773 | template: 774 | spec: 775 | containers: 776 | --- 777 | resources: 778 | requests: 779 | cpu: 200m 780 | limits: 781 | cpu: 500m 782 | ``` 783 | 784 | And recycle the deployment (Skaffold will do it for you). Then add an autoscaler: 785 | 786 | ``` 787 | $ kubectl autoscale deployment app --min=1 --max=3 788 | $ kubectl get hpa 789 | NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE 790 | app Deployment/app 5%/80% 1 3 1 9s 791 | ``` 792 | 793 | Hit the endpoints hard with (e.g.) Apache Bench: 794 | 795 | ``` 796 | $ ab -c 100 -n 10000 http://localhost:4503/actuator/ 797 | ``` 798 | 799 | and you should see it scale up: 800 | 801 | ``` 802 | $ kubectl get hpa 803 | NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE 804 | app Deployment/app 112%/80% 1 3 2 7m25s 805 | ``` 806 | 807 | and then back down: 808 | 809 | ``` 810 | $ kubectl get hpa 811 | NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE 812 | app Deployment/app 5%/80% 1 3 1 20m 813 | ``` 814 | 815 | > NOTE: If you update the app and it restarts or redeploys, the CPU activity on startup can trigger an autoscale up. Kind of nuts. It's potentially a thundering herd. 816 | 817 | The `kubectl autoscale` command generates a manifest for the "hpa" something like this: 818 | 819 | ```yaml 820 | apiVersion: autoscaling/v2beta2 821 | kind: HorizontalPodAutoscaler 822 | metadata: 823 | name: app 824 | spec: 825 | maxReplicas: 3 826 | metrics: 827 | - resource: 828 | name: cpu 829 | target: 830 | averageUtilization: 80 831 | type: Utilization 832 | type: Resource 833 | minReplicas: 1 834 | scaleTargetRef: 835 | apiVersion: apps/v1 836 | kind: Deployment 837 | name: app 838 | ``` 839 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Spring on Kubernetes Workshop 3 | tags: Templates, Talk 4 | description: This is a workshop that will show you how to run Spring apps on Kubernetes. 5 | --- 6 | 7 | # Spring on Kubernetes! 8 | 9 | 10 | Workshop Materials: https://hackmd.io/@ryanjbaxter/spring-on-k8s-workshop 11 | Ryan Baxter, Spring Cloud Engineer, VMware 12 | Dave Syer, Spring Engineer, VMware 13 | 14 | --- 15 | 16 | ## What You Will Do 17 | 18 | * Create a basic Spring Boot app 19 | * Build a Docker image for the app 20 | * Push the app to a Docker repo 21 | * Create deployment and service descriptors for Kubernetes 22 | * Deploy and run the app on Kubernetes 23 | * External configuration and service discovery 24 | * Deploy the Spring PetClinic App with MySQL 25 | 26 | --- 27 | 28 | ### Prerequisites 29 | 30 | Everyone will need: 31 | 32 | * Basic knowledge of Spring and Kubernetes (we will not be giving an introduction to either) 33 | 34 | If you are following these notes from a KubeAdademy event all the pre-requisites will be provided in the Lab. You only need to worry about these if you are going to work through the lab on your own. 35 | 36 | * [JDK 8 or higher](https://openjdk.java.net/install/index.html) 37 | * **Please ensure you have a JDK installed and not just a JRE** 38 | * [Docker](https://docs.docker.com/install/) installed 39 | * [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) installed 40 | * [Kustomize](https://github.com/kubernetes-sigs/kustomize/blob/master/docs/INSTALL.md) installed 41 | * [Skaffold](https://skaffold.dev/docs/install/) installed 42 | * An IDE (IntelliJ, Eclipse, VSCode) 43 | * **Optional** [Cloud Code](https://cloud.google.com/code/) for IntelliJ and VSCode is nice. For VSCode the [Microsoft Kubernetes Extension](https://github.com/Azure/vscode-kubernetes-tools) is almost the same. 44 | 45 | 46 | --- 47 | 48 | ### Configure kubectl 49 | 50 | * Depending on how you are doing this workshop will determine how you configure `kubectl` 51 | 52 | #### Doing The Workshop On Your Own 53 | 54 | * If you are doing this workshop on your own you will need to have your own Kubernetes cluster and Docker repo that the cluster can access 55 | * **Docker Desktop and Docker Hub** - Docker Desktop allows you to easily setup a local Kubernetes cluster ([Mac](https://docs.docker.com/docker-for-mac/#kubernetes), [Windows](https://docs.docker.com/docker-for-windows/#kubernetes)). This in combination with [Docker Hub](https://hub.docker.com/) should allow you to easily run through this workshop. 56 | * **Hosted Kubernetes Clusters and Repos** - Various cloud providers such as Google and Amazon offer options for running Kubernetes clusters and repos in the cloud. You will need to follow instructions from the cloud provider to provision the cluster and repo as well configuring `kubectl` to work with these clusters. 57 | 58 | 59 | #### Doing The Workshop in Strigo 60 | 61 | * Login To Strigo with the link and access code provided by KubeAcademy. 62 | * Configuring `kubectl`. Run this command in the terminal: 63 | 64 | $ kind-setup 65 | Cluster already active: kind 66 | Setting up kubeconfig 67 | 68 | * Run the below command to verify kubectl is configured correctly 69 | 70 | ```bash 71 | $ kubectl cluster-info 72 | Kubernetes master is running at https://127.0.0.1:43723 73 | KubeDNS is running at https://127.0.0.1:43723/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy 74 | 75 | To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. 76 | ``` 77 | 78 | > NOTE: it might take a minute or so after the VM launches to get the Kubernetes API server up and running, so your first few attempts at using `kubectl` may be very slow or fail. After that it should be responsive. 79 | 80 | --- 81 | 82 | ## Create a Spring Boot App 83 | 84 | In the Lab: 85 | 86 | * Run these commands in your terminal (please copy them verbatim to make the rest of the lab run smoothly) 87 | 88 | ```bash 89 | $ cd demo 90 | $ curl https://start.spring.io/starter.tgz -d artifactId=k8s-demo-app -d name=k8s-demo-app -d packageName=com.example.demo -d dependencies=web,actuator -d javaVersion=11 | tar -xzf - 91 | ``` 92 | 93 | * Open the IDE using the "IDE" button at the top of the lab - it might be obscured by the "Call for Assistance" button. 94 | 95 | Working on your own: 96 | 97 | * Click [here](https://start.spring.io/starter.zip?type=maven-project&language=java&bootVersion=2.3.0.M4&packaging=jar&jvmVersion=1.8&groupId=com.example&artifactId=k8s-demo-app&name=k8s-demo-app&description=Demo%20project%20for%20Spring%20Boot%20and%20Kubernetes&packageName=com.example.demo&dependencies=web,actuator&javaVersion=11) to download a zip of the Spring Boot app 98 | * Unzip the project to your desired workspace and open in your favorite IDE 99 | 100 | --- 101 | 102 | ## Add A RestController 103 | 104 | Modify `K8sDemoApplication.java` and add a `@RestController` 105 | 106 | :::warning 107 | Be sure to add the `@RestController` annotation and not just the `@GetMapping` 108 | ::: 109 | 110 | ```java 111 | package com.example.demo; 112 | 113 | import org.springframework.boot.SpringApplication; 114 | import org.springframework.boot.autoconfigure.SpringBootApplication; 115 | import org.springframework.web.bind.annotation.GetMapping; 116 | import org.springframework.web.bind.annotation.RestController; 117 | 118 | @SpringBootApplication 119 | @RestController 120 | public class K8sDemoAppApplication { 121 | 122 | public static void main(String[] args) { 123 | SpringApplication.run(K8sDemoAppApplication.class, args); 124 | } 125 | 126 | @GetMapping("/") 127 | public String hello() { 128 | return "Hello World"; 129 | } 130 | } 131 | ``` 132 | 133 | --- 134 | 135 | ## Run The App 136 | 137 | In a terminal window run 138 | 139 | ```bash 140 | $ ./mvnw clean package && java -jar ./target/*.jar 141 | ``` 142 | 143 | The app will start on port `8080` 144 | 145 | --- 146 | 147 | ## Test The App 148 | 149 | Make an HTTP request to http://localhost:8080 in another terminal 150 | 151 | ```bash 152 | $ curl http://localhost:8080; echo 153 | Hello World 154 | ``` 155 | 156 | --- 157 | 158 | ## Test Spring Boot Actuator 159 | 160 | Spring Boot Actuator adds several other endpoints to our app 161 | 162 | ```bash 163 | $ curl localhost:8080/actuator | jq . 164 | { 165 | "_links": { 166 | "self": { 167 | "href": "http://localhost:8080/actuator", 168 | "templated": false 169 | }, 170 | "health": { 171 | "href": "http://localhost:8080/actuator/health", 172 | "templated": false 173 | }, 174 | "info": { 175 | "href": "http://localhost:8080/actuator/info", 176 | "templated": false 177 | } 178 | } 179 | ``` 180 | 181 | :::warning 182 | Be sure to stop the Java process before continuing on or else you might get port binding issues since Java is using port `8080` 183 | ::: 184 | 185 | --- 186 | 187 | ## Containerize The App 188 | 189 | The first step in running the app on Kubernetes is producing a container for the app we can then deploy to Kubernetes 190 | 191 | ![](https://i.imgur.com/nM8G7ag.png) 192 | 193 | --- 194 | 195 | ### Building A Container 196 | 197 | * Spring Boot 2.3.x can build a container for you without the need for any additional plugins or files 198 | * To do this use the Spring Boot Build plugin goal `build-image` 199 | 200 | ```bash 201 | $ ./mvnw spring-boot:build-image 202 | ``` 203 | 204 | * Running `docker images` will allow you to see the built container 205 | 206 | ```bash 207 | $ docker images 208 | REPOSITORY TAG IMAGE ID CREATED SIZE 209 | k8s-demo-app 0.0.1-SNAPSHOT ab449be57b9d 5 minutes ago 124MB 210 | ``` 211 | 212 | --- 213 | 214 | ### Run The Container 215 | 216 | ```bash 217 | $ docker run --name k8s-demo-app -p 8080:8080 k8s-demo-app:0.0.1-SNAPSHOT 218 | ``` 219 | 220 | --- 221 | 222 | ### Test The App Responds 223 | 224 | ```bash 225 | $ curl http://localhost:8080; echo 226 | Hello World 227 | ``` 228 | 229 | :::warning 230 | Be sure to stop the docker container before continuing. You can stop the container and remove it by running `$ docker rm -f k8s-demo-app 231 | ` 232 | ::: 233 | 234 | 235 | --- 236 | 237 | ### Putting The Container In A Registry 238 | 239 | * Up until this point the container only lives on your machine 240 | * It is useful to instead place the container in a registry 241 | * Allows others to use the container 242 | * [Docker Hub](https://hub.docker.com/) is a popular public registry 243 | * Private registries exist as well. In this lab you will be using a private registry on localhost. 244 | 245 | --- 246 | 247 | ### Run The Build And Deploy The Container 248 | 249 | * You should be able to run the Maven build and push the container to the local container registry 250 | 251 | ```bash 252 | $ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=localhost:5000/apps/demo 253 | $ docker push localhost:5000/apps/demo 254 | ``` 255 | 256 | * You can now see the image in the registry 257 | 258 | ```bash 259 | $ curl localhost:5000/v2/_catalog 260 | {"repositories":["apps/demo"]} 261 | ``` 262 | 263 | --- 264 | 265 | ## Deploying To Kubernetes 266 | 267 | * With our container build and deployed to a registry you can now run this container on Kubernetes 268 | 269 | --- 270 | 271 | ### Deployment Descriptor 272 | 273 | * Kubernetes uses YAML files to provide a way of describing how the app will be deployed to the platform 274 | * You can write these by hand using the Kubernetes documentation as a reference 275 | * Or you can have Kubernetes generate it for you using kubectl 276 | * The `--dry-run` flag allows us to generate the YAML without actually deploying anything to Kubernetes 277 | 278 | ```bash 279 | $ mkdir k8s 280 | $ kubectl create deployment k8s-demo-app --image localhost:5000/apps/demo -o yaml --dry-run > k8s/deployment.yaml 281 | ``` 282 | 283 | * The resulting `deployment.yaml` should look similar to this 284 | 285 | ```yaml 286 | apiVersion: apps/v1 287 | kind: Deployment 288 | metadata: 289 | creationTimestamp: null 290 | labels: 291 | app: k8s-demo-app 292 | name: k8s-demo-app 293 | spec: 294 | replicas: 1 295 | selector: 296 | matchLabels: 297 | app: k8s-demo-app 298 | strategy: {} 299 | template: 300 | metadata: 301 | creationTimestamp: null 302 | labels: 303 | app: k8s-demo-app 304 | spec: 305 | containers: 306 | - image: localhost:5000/apps/demo 307 | name: k8s-demo-app 308 | resources: {} 309 | status: {} 310 | ``` 311 | 312 | --- 313 | 314 | ### Service Descriptor 315 | 316 | * A service acts as a load balancer for the pod created by the deployment descriptor 317 | * If we want to be able to scale pods than we want to create a service for those pods 318 | 319 | ```bash 320 | $ kubectl create service clusterip k8s-demo-app --tcp 80:8080 -o yaml --dry-run > k8s/service.yaml 321 | ``` 322 | 323 | * The resulting `service.yaml` should look similar to this 324 | ```yaml 325 | apiVersion: v1 326 | kind: Service 327 | metadata: 328 | creationTimestamp: null 329 | labels: 330 | app: k8s-demo-app 331 | name: k8s-demo-app 332 | spec: 333 | ports: 334 | - name: 80-8080 335 | port: 80 336 | protocol: TCP 337 | targetPort: 8080 338 | selector: 339 | app: k8s-demo-app 340 | type: ClusterIP 341 | status: 342 | loadBalancer: {} 343 | ``` 344 | 345 | --- 346 | 347 | ### Apply The Deployment and Service YAML 348 | 349 | * The deployment and service descriptors have been created in the `/k8s` directory 350 | * Apply these to get everything running 351 | * If you have `watch` installed you can watch as the pods and services get created 352 | 353 | ```bash 354 | $ watch -n 1 kubectl get all 355 | ``` 356 | 357 | * In a separate terminal window run 358 | 359 | ```bash 360 | $ kubectl apply -f ./k8s 361 | ``` 362 | 363 | ```bash 364 | Every 1.0s: kubectl get all Ryans-MacBook-Pro.local: Wed Jan 29 17:23:28 2020 365 | 366 | NAME READY STATUS RESTARTS AGE 367 | pod/k8s-demo-app-d6dd4c4d4-7t8q5 1/1 Running 0 68m 368 | 369 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 370 | service/k8s-demo-app ClusterIP 10.100.200.243 80/TCP 68m 371 | 372 | NAME READY UP-TO-DATE AVAILABLE AGE 373 | deployment.apps/k8s-demo-app 1/1 1 1 68m 374 | 375 | NAME DESIRED CURRENT READY AGE 376 | replicaset.apps/k8s-demo-app-d6dd4c4d4 1 1 1 68m 377 | ``` 378 | 379 | :::info 380 | `watch` is a useful command line tool that you can install on [Linux](https://www.2daygeek.com/linux-watch-command-to-monitor-a-command/) and [OSX](https://osxdaily.com/2010/08/22/install-watch-command-on-os-x/). All it does is continuously executes the command you pass it. You can just run the `kubectl` command specified after the `watch` command but the output will be static as opposed to updating constantly. 381 | ::: 382 | 383 | --- 384 | 385 | ### Testing The App 386 | 387 | * The service is assigned a cluster IP, which is only accessible from inside the cluster 388 | 389 | ``` 390 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 391 | service/k8s-demo-app ClusterIP 10.100.200.243 80/TCP 68m 392 | ``` 393 | 394 | * To access the app we can use `kubectl port-forward` 395 | 396 | ```bash 397 | $ kubectl port-forward service/k8s-demo-app 8080:80 398 | ``` 399 | 400 | * Now we can `curl` localhost:8080 and it will be forwarded to the service in the cluster 401 | 402 | ```bash 403 | $ curl http://localhost:8080; echo 404 | Hello World 405 | ``` 406 | 407 | :::success 408 | **Congrats you have deployed your first app to Kubernetes 🎉** 409 | 410 |

via GIPHY

411 | ::: 412 | 413 | :::warning 414 | Be sure to stop the `kubectl port-forward` process before moving on 415 | ::: 416 | 417 | --- 418 | 419 | ### Exposing The Service 420 | 421 | > NOTE: `LoadBalancer` features are platform specific. The visibility of your app after changing the service type might depend a lot on where it is deployed (e.g. per cloud provider). 422 | 423 | * If we want to expose the service publically we can change the service type to `LoadBalancer` 424 | * Open `k8s/service.yaml` and change `ClusterIp` to `LoadBalancer` 425 | 426 | ```yaml 427 | apiVersion: v1 428 | kind: Service 429 | metadata: 430 | labels: 431 | app: k8s-demo-app 432 | name: k8s-demo-app 433 | spec: 434 | ... 435 | type: LoadBalancer 436 | ... 437 | ``` 438 | 439 | * Now apply the updated `service.yaml` 440 | 441 | ```bash 442 | $ kubectl apply -f ./k8s 443 | ``` 444 | 445 | --- 446 | 447 | ### Testing The Public LoadBalancer 448 | 449 | * In a Cloud environment (Google, Amazon, Azure etc.), Kubernetes will assign the service an external ip 450 | * It may take a minute or so for Kubernetes to assign the service an external IP, until it is assigned you might see `` in the `EXTERNAL-IP` column 451 | * For a local cluster we need to manually set the external IP address to the IP address of the Kubernetes node (the docker container running Kind in this case): 452 | ```bash 453 | $ kubectl patch service k8s-demo-app -p '{"spec": {"type": "LoadBalancer", "externalIPs":["172.18.0.2"]}}' 454 | ``` 455 | 456 | ```bash 457 | $ kubectl get service k8s-demo-app -w 458 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 459 | k8s-demo-app LoadBalancer 10.100.200.243 172.18.0.2 80:31428/TCP 85m 460 | $ curl http://172.18.0.2; echo 461 | Hello World 462 | ``` 463 | 464 | :::info 465 | The `-w` option of `kubectl` lets you watch a single Kubernetes resource. 466 | ::: 467 | 468 | --- 469 | 470 | ## Best Practices 471 | 472 | ### Liveness and Readiness Probes 473 | 474 | * Kubernetes uses two probes to determine if the app is ready to accept traffic and whether the app is alive 475 | * If the readiness probe does not return a `200` no trafic will be routed to it 476 | * If the liveness probe does not return a `200` kubernetes will restart the Pod 477 | * Spring Boot has a build in set of endpoints from the [Actuator](https://spring.io/blog/2020/03/25/liveness-and-readiness-probes-with-spring-boot) module that fit nicely into these use cases 478 | * The `/health/readiness` endpoint indicates if the application is healthy, this fits with the readiness proble 479 | * The `/health/liveness` endpoint serves application info, we can use this to make sure the application is "alive" 480 | 481 | :::info 482 | The `/health/readiness` and `/health/liveness` endpoints are only available in Spring Boot 2.3.x. The`/health` and `/info` endpoints are reasonable starting points in earlier versions. 483 | ::: 484 | 485 | --- 486 | 487 | ### Add The Readiness Probe 488 | 489 | ```yaml 490 | apiVersion: apps/v1 491 | kind: Deployment 492 | metadata: 493 | ... 494 | name: k8s-demo-app 495 | spec: 496 | ... 497 | template: 498 | ... 499 | spec: 500 | containers: 501 | ... 502 | readinessProbe: 503 | httpGet: 504 | port: 8080 505 | path: /actuator/health/readiness 506 | ``` 507 | 508 | --- 509 | 510 | ### Add The Liveness Probe 511 | 512 | ```yaml 513 | apiVersion: apps/v1 514 | kind: Deployment 515 | metadata: 516 | ... 517 | name: k8s-demo-app 518 | spec: 519 | ... 520 | template: 521 | ... 522 | spec: 523 | containers: 524 | ... 525 | livenessProbe: 526 | httpGet: 527 | port: 8080 528 | path: /actuator/health/liveness 529 | ``` 530 | 531 | --- 532 | 533 | ### Graceful Shutdown 534 | 535 | * Due to the asynchronous way Kubnernetes shuts down applications there is a period of time when requests can be sent to the application while an application is being terminated. 536 | * To deal with this we can configure a pre-stop sleep to allow enough time for requests to stop being routed to the application before it is terminated. 537 | * Add a `preStop` command to the `podspec` of your `deployment.yaml` 538 | 539 | ```yaml 540 | apiVersion: apps/v1 541 | kind: Deployment 542 | metadata: 543 | ... 544 | name: k8s-demo-app 545 | spec: 546 | ... 547 | template: 548 | ... 549 | spec: 550 | containers: 551 | ... 552 | lifecycle: 553 | preStop: 554 | exec: 555 | command: ["sh", "-c", "sleep 10"] 556 | ``` 557 | 558 | --- 559 | ### Handling In Flight Requests 560 | 561 | * Our application could also be handling requests when it receives the notification that it need to shut down. 562 | * In order for us to finish processing those requests before the applicaiton shuts down we can configure a "grace period" in our Spring Boot applicaiton. 563 | * Open `application.properties` in `/src/main/resources` and add 564 | 565 | ```properties 566 | server.shutdown=graceful 567 | ``` 568 | 569 | There is also a `spring.lifecycle.timeout-per-shutdown-phase` (default 30s). 570 | 571 | :::info 572 | `server.shutdown` is only available begining in Spring Boot 2.3.x 573 | ::: 574 | 575 | --- 576 | 577 | ### Update The Container & Apply The Updated Deployment YAML 578 | 579 | Let's update the `pom.xml` to configure the image name explicitly: 580 | 581 | ``` 582 | 583 | ... 584 | localhost:5000/apps/demo 585 | 586 | ``` 587 | 588 | Then we can build and push the changes and re-deploy: 589 | 590 | ```bash 591 | $ ./mvnw clean spring-boot:build-image 592 | $ docker push localhost:5000/apps/demo 593 | $ kubectl apply -f ./k8s 594 | ``` 595 | 596 | * An updated Pod will be created and started and the old one will be terminated 597 | * If you use `watch -n 1 kubectl get all` to see all the Kubernetes resources you will be able to see this appen in real time 598 | 599 | --- 600 | 601 | 644 | 645 | ### Cleaning Up 646 | 647 | * Before we move on to the next section lets clean up everything we deployed 648 | 649 | ```bash 650 | $ kubectl delete -f ./k8s 651 | ``` 652 | 653 | --- 654 | 655 | ## Skaffold 656 | 657 | * [Skaffold](https://github.com/GoogleContainerTools/skaffold) is a command line tool that facilitates continuous development for Kubernetes applications 658 | * Simplifies the development process by combining multiple steps into one easy command 659 | * Provides the building blocks for a CI/CD process 660 | * Make sure you have Skaffold installed before continuing 661 | 662 | ```bash 663 | $ skaffold version 664 | v1.9.1 665 | ``` 666 | 667 | ### Adding Skaffold YAML 668 | 669 | * Skaffold is configured using...you guessed it...another YAML file 670 | * We create a YAML file `skaffold.yaml` in the root of the project 671 | 672 | ```bash 673 | apiVersion: skaffold/v2beta3 674 | kind: Config 675 | metadata: 676 | name: k-s-demo-app-- 677 | build: 678 | artifacts: 679 | - image: localhost:5000/apps/demo 680 | custom: 681 | buildCommand: ./mvnw spring-boot:build-image -D spring-boot.build-image.imageName=$IMAGE && docker push $IMAGE 682 | dependencies: 683 | paths: 684 | - src 685 | - pom.xml 686 | deploy: 687 | kubectl: 688 | manifests: 689 | - k8s/deployment.yaml 690 | - k8s/service.yaml 691 | ``` 692 | 693 | --- 694 | 695 | ### Development With Skaffold 696 | 697 | * Skaffold makes some enhacements to our development workflow when using Kubernetes 698 | * Skaffold will 699 | * Build the app (Maven) 700 | * Create the container (Spring Boot) 701 | * Push the container to the registry (Docker) 702 | * Apply the deployment and service YAMLs 703 | * Stream the logs from the Pod to your terminal 704 | * Automatically setup port forwarding 705 | ```bash 706 | $ skaffold dev --port-forward 707 | ``` 708 | 709 | --- 710 | 711 | ### Testing Everything Out 712 | 713 | * If you are `watch`ing your Kubernetes resources you will see the same resources created as before 714 | * When running `skaffold dev --port-forward` you will see a line in your console that looks like 715 | 716 | ``` 717 | Port forwarding service/k8s-demo-app in namespace rbaxter, remote port 80 -> address 127.0.0.1 port 4503 718 | ``` 719 | 720 | * In this case port `4503` will be forwarded to port `80` of the service 721 | 722 | ```bash 723 | $ curl localhost:4503; echo 724 | Hello World 725 | ``` 726 | --- 727 | 728 | ### Make Changes To The Controller 729 | 730 | * Skaffold is watching the project files for changes 731 | * Open `K8sDemoApplication.java` and change the `hello` method to return `Hola World` 732 | * Once you save the file you will notice Skaffold rebuild and redeploy everthing with the new change 733 | 734 | ```bash 735 | $ curl localhost:4503; echo 736 | Hola World 737 | ``` 738 | 739 | --- 740 | 741 | ### Cleaning Everything Up 742 | 743 | * Once we are done, if we kill the `skaffold` process, skaffold will remove the deployment and service resources. Just hit `CTRL-C` in the terminal where `skaffold` is running.. 744 | ```bash 745 | ... 746 | WARN[2086] exit status 1 747 | Cleaning up... 748 | - deployment.apps "k8s-demo-app" deleted 749 | - service "k8s-demo-app" deleted 750 | ``` 751 | 752 | --- 753 | 754 | ### Debugging With Skaffold 755 | 756 | * Skaffold also makes it easy to attach a debugger to the container running in Kubernetes 757 | 758 | ```bash 759 | $ skaffold debug --port-forward 760 | ... 761 | Port forwarding service/k8s-demo-app in namespace rbaxter, remote port 80 -> address 127.0.0.1 port 4503 762 | Watching for changes... 763 | Port forwarding pod/k8s-demo-app-75d4f4b664-2jqvx in namespace rbaxter, remote port 5005 -> address 127.0.0.1 port 5005 764 | ... 765 | ``` 766 | 767 | * The `debug` command results in two ports being forwarded 768 | * The http port, `4503` in the above example 769 | * The remote debug port `5005` in the above example 770 | * You can then setup the remote debug configuration in your IDE to attach to the process and set breakpoints just like you would if the app was running locally 771 | * If you set a breakpoint where we return `Hola World` from the `hello` method in `K8sDemoApplication.java` and then issue our `curl` command to hit the endpoint you should be able to step through the code 772 | 773 | ![](https://i.imgur.com/4KK549f.png) 774 | 775 | :::warning 776 | Be sure to detach the debugger and kill the `skaffold` process before continuing 777 | ::: 778 | 779 | --- 780 | 781 | ## Kustomize 782 | 783 | * [Kustomize](https://kustomize.io/) another tool we can use in our Kubernetes toolbox that allows us to customize deployments to different environments 784 | * We can start with a base set of resources and then apply customizations on top of those 785 | * Features 786 | * Allows easier deployments to different environments/providers 787 | * Allows you to keep all the common properties in one place 788 | * Generate configuration for specific environments 789 | * No templates, no placeholder spaghetti, no environment variable overload 790 | 791 | --- 792 | 793 | ### Getting Started With Kustomize 794 | 795 | * Create a new directory in the root of our project called `kustomize/base` 796 | * Move the `deployment.yaml` and `service.yaml` from the `k8s` directory into `kustomize/base` 797 | * Delete the `k8s` directory 798 | 799 | ```bash 800 | $ mkdir -p kustomize/base 801 | $ mv k8s/* kustomize/base 802 | $ rm -Rf k8s 803 | ``` 804 | 805 | --- 806 | 807 | ### kustomize.yaml 808 | 809 | * In `kustomize/base` create a new file called `kustomization.yaml` and add the following to it 810 | ```yaml 811 | apiVersion: kustomize.config.k8s.io/v1beta1 812 | kind: Kustomization 813 | 814 | resources: 815 | - service.yaml 816 | - deployment.yaml 817 | ``` 818 | 819 | > NOTE: Optionally, you can now remove all the labels and annotations in the metadata of both objects and specs inside objects. Kustomize adds default values that link a service to a deployment. If there is only one of each in your manifest then it will pick something sensible. 820 | 821 | --- 822 | 823 | ### Customizing Our Deployment 824 | 825 | * Lets imagine we want to deploy our app to a QA environment, but in this environment we want to have two instances of our app running 826 | * Create a new directory called `qa` under the `kustomize` directory 827 | * Create a new file in `kustomize/qa` called `update-replicas.yaml`, this is where we will provide customizations for our QA environment 828 | * Add the following content to `kustomize/qa/update-replicas.yaml` 829 | ```yaml 830 | apiVersion: apps/v1 831 | kind: Deployment 832 | metadata: 833 | name: k8s-demo-app 834 | spec: 835 | replicas: 2 836 | ``` 837 | 838 | * Create a new file called `kustomization.yaml` in `kustomize/qa` and add the following to it 839 | 840 | ```yaml 841 | apiVersion: kustomize.config.k8s.io/v1beta1 842 | kind: Kustomization 843 | 844 | resources: 845 | - ../base 846 | 847 | patchesStrategicMerge: 848 | - update-replicas.yaml 849 | ``` 850 | 851 | * Here we tell Kustomize that we want to patch the resources from the `base` directory with the `update-replicas.yaml` file 852 | * Notice that in `update-replicas.yaml` we are just updating the properties we care about, in this case the `replicas` 853 | 854 | --- 855 | 856 | ### Running Kustomize 857 | 858 | * You will need to have [Kustomize installed](https://github.com/kubernetes-sigs/kustomize/blob/master/docs/INSTALL.md) 859 | 860 | ```bash 861 | $ kustomize build ./kustomize/base 862 | ``` 863 | 864 | * This is our base deployment and service resources 865 | 866 | ```bash 867 | $ kustomize build ./kustomize/qa 868 | ... 869 | spec: 870 | replicas: 2 871 | ... 872 | ``` 873 | 874 | * Notice when we build the QA customization that the replicas property is updated to `2` 875 | 876 | --- 877 | 878 | ### Piping Kustomize Into Kubectl 879 | 880 | * We can pipe the output from `kustomize` into `kubectl` in order to use the generated YAML to deploy the app to Kubernetes 881 | 882 | ```bash 883 | $ kustomize build kustomize/qa | kubectl apply -f - 884 | ``` 885 | 886 | * If you are watching the pods in your Kubernetes namespace you will now see two pods created instead of one 887 | 888 | ```bash 889 | Every 1.0s: kubectl get all Ryans-MacBook-Pro.local: Mon Feb 3 12:00:04 2020 890 | 891 | NAME READY STATUS RESTARTS AGE 892 | pod/k8s-demo-app-647b8d5b7b-r2999 1/1 Running 0 83s 893 | pod/k8s-demo-app-647b8d5b7b-x4t54 1/1 Running 0 83s 894 | 895 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 896 | service/k8s-demo-app ClusterIP 10.100.200.200 80/TCP 84s 897 | 898 | NAME READY UP-TO-DATE AVAILABLE AGE 899 | deployment.apps/k8s-demo-app 2/2 2 2 84s 900 | 901 | NAME DESIRED CURRENT READY AGE 902 | replicaset.apps/k8s-demo-app-647b8d5b7b 2 2 2 84s 903 | ``` 904 | 905 | :::success 906 | Our service `k8s-demo-app` will load balance requests between these two pods 907 | ::: 908 | 909 | --- 910 | 911 | ### Clean Up 912 | 913 | * Before continuing clean up your Kubernetes environment 914 | 915 | ```bash 916 | $ kustomize build kustomize/qa | kubectl delete -f - 917 | ``` 918 | --- 919 | 920 | ### Using Kustomize With Skaffold 921 | 922 | * Currently our Skaffold configuration uses `kubectl` to deploy our artifacts, but we can change that to use `kustomize` 923 | * Change your `skaffold.yaml` to the following 924 | :::warning 925 | Make sure you replace `[USERNAME]` in the image property with your own username 926 | ::: 927 | ```yaml 928 | apiVersion: skaffold/v2beta3 929 | kind: Config 930 | metadata: 931 | name: k-s-demo-app 932 | build: 933 | artifacts: 934 | - image: localhost:5000/apps/demo 935 | custom: 936 | buildCommand: ./mvnw spring-boot:build-image -D spring-boot.build-image.imageName=$IMAGE && docker push $IMAGE 937 | dependencies: 938 | paths: 939 | - src 940 | - pom.xml 941 | deploy: 942 | kustomize: 943 | paths: ["kustomize/base"] 944 | profiles: 945 | - name: qa 946 | deploy: 947 | kustomize: 948 | paths: ["kustomize/qa"] 949 | ``` 950 | 951 | --- 952 | 953 | * Notice now the `deploy` property has been changed from `kubectl` to now use Kustomize 954 | * Also notice that we have a new profiles section allowing us to deploy our QA configuration using Skaffold 955 | 956 | --- 957 | 958 | ### Testing Skaffold + Kustomize 959 | 960 | * If you run your normal `skaffold` commands it will use the deployment configuration from `kustomize/base` 961 | 962 | ```bash 963 | $ skaffold dev --port-forward 964 | ``` 965 | 966 | * If you want to test out the QA deployment run the following command to activate the QA profile 967 | 968 | ```bash 969 | $ skaffold dev -p qa --port-forward 970 | ``` 971 | 972 | :::warning 973 | Be sure to kill the `skaffold` process before continuing 974 | ::: 975 | --- 976 | 977 | ## Externalized Configuration 978 | 979 | * One of the [12 factors for cloud native apps](https://12factor.net/config) is to externalize configuration 980 | * Kubernetes provides support for externalizing configuration via config maps and secrets 981 | * We can create a config map or secret easily using `kubectl` 982 | 983 | ```bash 984 | $ kubectl create configmap log-level --from-literal=LOGGING_LEVEL_ORG_SPRINGFRAMEWORK=DEBUG 985 | $ kubectl get configmap log-level -o yaml 986 | apiVersion: v1 987 | data: 988 | LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: DEBUG 989 | kind: ConfigMap 990 | metadata: 991 | creationTimestamp: "2020-02-04T15:51:03Z" 992 | name: log-level 993 | namespace: rbaxter 994 | resourceVersion: "2145271" 995 | selfLink: /api/v1/namespaces/default/configmaps/log-level 996 | uid: 742f3d2a-ccd6-4de1-b2ba-d1514b223868 997 | ``` 998 | 999 | --- 1000 | 1001 | ### Using Config Maps In Our Apps 1002 | 1003 | * There are a number of ways to consume the data from config maps in our apps 1004 | * Perhaps the easiest is to use the data as environment variables 1005 | * To do this we need to change our `deployment.yaml` in `kustomize/base` 1006 | ```yaml 1007 | apiVersion: apps/v1 1008 | kind: Deployment 1009 | ... 1010 | spec: 1011 | ... 1012 | template: 1013 | ... 1014 | spec: 1015 | containers: 1016 | - image: localhost:5000/apps/demo 1017 | name: k8s-demo-app 1018 | envFrom: 1019 | - configMapRef: 1020 | name: log-level 1021 | ... 1022 | ``` 1023 | 1024 | * Add the `envFrom` properties above which reference our config map `log-level` 1025 | * Update the deployment by running `skaffold dev` (so we can stream the logs) 1026 | * If everything worked correctly you should see much more verbose logging in your console 1027 | 1028 | --- 1029 | 1030 | ### Removing The Config Map and Reverting The Deployment 1031 | 1032 | * Before continuing lets remove our config map and revert the changes we made to `deployment.yaml` 1033 | * To delete the config map run the following command 1034 | ```bash 1035 | $ kubectl delete configmap log-level 1036 | ``` 1037 | * In `kustomize/base/deployment.yaml` remove the `envFrom` properties we added 1038 | * Next we will use Kustomize to make generating config maps easier 1039 | 1040 | --- 1041 | 1042 | ### Config Maps and Spring Boot Application Configuration 1043 | 1044 | * In Spring Boot we usually place our configuration values in application properties or YAML 1045 | * Config Maps in Kubernetes can be populated with values from files, like properties or YAML files 1046 | * We can do this via `kubectl` 1047 | ```bash 1048 | $ kubectl create configmap k8s-demo-app-config --from-file ./path/to/application.yaml 1049 | ``` 1050 | :::warning 1051 | No need to execute the above command, it is just an example, the following sections will show a better way 1052 | ::: 1053 | * We can then mount this config map as a volume in our container at a directory Spring Boot knows about and Spring Boot will automatically recognize the file and use it 1054 | 1055 | --- 1056 | 1057 | ### Creating A Config Map With Kustomize 1058 | 1059 | * Kustomize offers a way of generating config maps and secrets as part of our customizations 1060 | * Create a file called `application.yaml` in `kustomize/base` and add the following content 1061 | 1062 | ```yaml 1063 | logging: 1064 | level: 1065 | org: 1066 | springframework: INFO 1067 | ``` 1068 | 1069 | * We can now tell Kustomize to generate a config map from this file, in `kustomize/base/kustomization.yaml` by adding the following snippet to the end of the file 1070 | 1071 | ```yaml 1072 | configMapGenerator: 1073 | - name: k8s-demo-app-config 1074 | files: 1075 | - application.yaml 1076 | ``` 1077 | 1078 | * If you now run `$ kustomize build` you will see a config map resource is produced 1079 | 1080 | ```bash 1081 | $ kustomize build kustomize/base 1082 | apiVersion: v1 1083 | data: 1084 | application.yaml: |- 1085 | logging: 1086 | level: 1087 | org: 1088 | springframework: INFO 1089 | kind: ConfigMap 1090 | metadata: 1091 | name: k8s-demo-app-config-fcc4c2fmcd 1092 | ``` 1093 | 1094 | :::info 1095 | By default `kustomize` generates a random name suffix for the `ConfigMap`. Kustomize will take care of reconciling this when the `ConfigMap` is referenced in other places (ie in volumes). It does this to force a change to the `Deployment` and in turn force the app to be restarted by Kubernetes. This isn't always what you want, for instance if the `ConfigMap` and the `Deployment` are not in the same `Kustomization`. If you want to omit the random suffix, you can set `behavior=merge` (or `replace`) in the `configMapGenerator`. 1096 | 1097 | ::: 1098 | 1099 | * Now edit `deployment.yaml` in `kustomize/base` to have kubernetes create a volume for that config map and mount that volume in the container 1100 | 1101 | ```yaml 1102 | apiVersion: apps/v1 1103 | kind: Deployment 1104 | ... 1105 | spec: 1106 | ... 1107 | template: 1108 | ... 1109 | spec: 1110 | containers: 1111 | - image: harbor.workshop.demo.ryanjbaxter.com/rbaxter/k8s-demo-app 1112 | name: k8s-demo-app 1113 | volumeMounts: 1114 | - name: config-volume 1115 | mountPath: /config 1116 | ... 1117 | volumes: 1118 | - name: config-volume 1119 | configMap: 1120 | name: k8s-demo-app-config 1121 | ``` 1122 | 1123 | * In the above `deployment.yaml` we are creating a volume named `config-volume` from the config map named `k8s-demo-app-config` 1124 | * In the container we are mounting the volume named `config-volume` within the container at the path `/config` 1125 | * Spring Boot automatically looks in `./config` for application configuration and if present will use it (because the app is running in /) 1126 | 1127 | --- 1128 | 1129 | ### Testing Our New Deployment 1130 | 1131 | * If you run `$ skaffold dev --port-forward` everything should deploy as normal 1132 | * Check that the config map was generated 1133 | ```bash 1134 | $ kubectl get configmap 1135 | NAME DATA AGE 1136 | k8s-demo-app-config-fcc4c2fmcd 1 18s 1137 | ``` 1138 | 1139 | * Skaffold is watching our files for changes, go ahead and change `logging.level.org.springframework` from `INFO` to `DEBUG` and Skaffold will automatically create a new config map and restart the pod 1140 | * You should see a lot more logging in your terminal once the new pod starts 1141 | 1142 | :::warning 1143 | Be sure to kill the `skaffold` process before continuing 1144 | ::: 1145 | 1146 | --- 1147 | 1148 | ## Service Discovery 1149 | 1150 | * Kubernetes makes it easy to make requests to other services 1151 | * Each service has a DNS entry in the container of the other services allowing you to make requests to that service using the service name 1152 | * For example, if there is a service called `k8s-workshop-name-service` deployed we could make a request from the `k8s-demo-app` just by making an HTTP request to `http://k8s-workshop-name-service` 1153 | 1154 | --- 1155 | 1156 | ### Deploying Another App 1157 | 1158 | * In order to save time we will use an [existing app](https://github.com/ryanjbaxter/k8s-spring-workshop/tree/master/name-service) that returns a random name 1159 | * The container for this service resides [in Docker Hub](https://hub.docker.com/repository/docker/ryanjbaxter/k8s-workshop-name-service) (a public container registery) 1160 | * To make things easier we placed a [Kustomization file](https://github.com/ryanjbaxter/k8s-spring-workshop/blob/master/name-service/kustomize/base/kustomization.yaml) in the GitHub repo that we can reference from our own Kustomization file to deploy the app to our cluster 1161 | 1162 | ### Modify `kustomization.yaml` 1163 | 1164 | * In your k8s-demo-app's `kustomize/base/kustomization.yaml` add a new resource pointing to the new app's `kustomize` directory 1165 | ```yaml 1166 | apiVersion: kustomize.config.k8s.io/v1beta1 1167 | kind: Kustomization 1168 | 1169 | resources: 1170 | - service.yaml 1171 | - deployment.yaml 1172 | - https://github.com/ryanjbaxter/k8s-spring-workshop/name-service/kustomize/base 1173 | 1174 | configMapGenerator: 1175 | - name: k8s-demo-app-config 1176 | files: 1177 | - application.yaml 1178 | ``` 1179 | 1180 | ### Making A Request To The Service 1181 | 1182 | * Modify the `hello` method of `K8sDemoApplication.java` to make a request to the new service 1183 | ```java= 1184 | package com.example.demo; 1185 | 1186 | import org.springframework.boot.SpringApplication; 1187 | import org.springframework.boot.autoconfigure.SpringBootApplication; 1188 | import org.springframework.boot.web.client.RestTemplateBuilder; 1189 | import org.springframework.web.bind.annotation.GetMapping; 1190 | import org.springframework.web.bind.annotation.RestController; 1191 | import org.springframework.web.client.RestTemplate; 1192 | 1193 | @SpringBootApplication 1194 | @RestController 1195 | public class K8sDemoAppApplication { 1196 | 1197 | private RestTemplate rest = new RestTemplateBuilder().build(); 1198 | 1199 | public static void main(String[] args) { 1200 | SpringApplication.run(K8sDemoAppApplication.class, args); 1201 | } 1202 | 1203 | @GetMapping("/") 1204 | public String hello() { 1205 | String name = rest.getForObject("http://k8s-workshop-name-service", String.class); 1206 | return "Hola " + name; 1207 | } 1208 | } 1209 | ``` 1210 | 1211 | * Notice the hostname of the request we are making matches the service name in [our `service.yaml` file](https://github.com/ryanjbaxter/k8s-spring-workshop/blob/master/name-service/kustomize/base/service.yaml#L7) 1212 | 1213 | 1214 | --- 1215 | 1216 | ### Testing The App 1217 | 1218 | * Deploy the apps using Skaffold 1219 | ```bash 1220 | $ skaffold dev --port-forward 1221 | ``` 1222 | 1223 | * This should deploy both the k8s-demo-app and the name-service app 1224 | ```bash 1225 | $ kubectl get all 1226 | NAME READY STATUS RESTARTS AGE 1227 | pod/k8s-demo-app-5b957cf66d-w7r9d 1/1 Running 0 172m 1228 | pod/k8s-workshop-name-service-79475f456d-4lqgl 1/1 Running 0 173m 1229 | 1230 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 1231 | service/k8s-demo-app LoadBalancer 10.100.200.102 35.238.231.79 80:30068/TCP 173m 1232 | service/k8s-workshop-name-service ClusterIP 10.100.200.16 80/TCP 173m 1233 | 1234 | NAME READY UP-TO-DATE AVAILABLE AGE 1235 | deployment.apps/k8s-demo-app 1/1 1 1 173m 1236 | deployment.apps/k8s-workshop-name-service 1/1 1 1 173m 1237 | 1238 | NAME DESIRED CURRENT READY AGE 1239 | replicaset.apps/k8s-demo-app-5b957cf66d 1 1 1 172m 1240 | replicaset.apps/k8s-demo-app-fd497cdfd 0 0 0 173m 1241 | replicaset.apps/k8s-workshop-name-service-79475f456d 1 1 1 173m 1242 | ``` 1243 | 1244 | * Because we deployed two services and supplied the `-port-forward` flag Skaffold will forward two ports 1245 | 1246 | ```bash 1247 | Port forwarding service/k8s-demo-app in namespace user1, remote port 80 -> address 127.0.0.1 port 4503 1248 | Port forwarding service/k8s-workshop-name-service in namespace user1, remote port 80 -> address 127.0.0.1 port 4504 1249 | ``` 1250 | 1251 | * Test the name service 1252 | ```bash 1253 | $ curl localhost:4504; echo 1254 | John 1255 | ``` 1256 | :::info 1257 | Hitting the service multiple times will return a different name 1258 | ::: 1259 | 1260 | * Test the k8s-demo-app which should now make a request to the name-service 1261 | ```bash 1262 | $ curl localhost:4503; echo 1263 | Hola John 1264 | ``` 1265 | :::info 1266 | Making multiple requests should result in different names coming from the name-service 1267 | ::: 1268 | 1269 | * Stop the Skaffold process to clean everything up before moving to the next step 1270 | 1271 | --- 1272 | 1273 | ## Running The PetClinic App 1274 | 1275 | * The [PetClinic app](https://github.com/spring-projects/spring-petclinic) is a popular demo app which leverages several Spring technologies 1276 | * Spring Data (using MySQL) 1277 | * Spring Caching 1278 | * Spring WebMVC 1279 | 1280 | ![](https://i.imgur.com/MCgAL4n.png) 1281 | 1282 | --- 1283 | 1284 | ### Deploying PetClinic 1285 | 1286 | * We have a Kustomization that we can use to easily get it up and running 1287 | 1288 | ```bash 1289 | $ kustomize build https://github.com/dsyer/docker-services/layers/samples/petclinic | kubectl apply -f - 1290 | $ kubectl port-forward service/petclinic-app 8080:80 1291 | ``` 1292 | 1293 | :::warning 1294 | The above `kustomize build` command may take some time to complete 1295 | ::: 1296 | 1297 | * Head to `http://localhost:8080` and you should see the "Welcome" page 1298 | * To use the app you can go to "Find Owners", add yourself, and add your pets 1299 | * All this data will be stored in the MYSQL database 1300 | 1301 | --- 1302 | 1303 | ### Dissecting PetClinic 1304 | 1305 | * Here's the `kustomization.yaml` that you just deployed: 1306 | 1307 | ```yaml 1308 | apiVersion: kustomize.config.k8s.io/v1beta1 1309 | kind: Kustomization 1310 | resources: 1311 | - ../../base 1312 | - ../../mysql 1313 | namePrefix: petclinic- 1314 | transformers: 1315 | - ../../mysql/transformer 1316 | - ../../actuator 1317 | - ../../prometheus 1318 | images: 1319 | - name: dsyer/template 1320 | newName: dsyer/petclinic 1321 | configMapGenerator: 1322 | - name: env-config 1323 | behavior: merge 1324 | literals: 1325 | - SPRING_CONFIG_LOCATION=classpath:/,file:///config/bindings/mysql/meta/ 1326 | - MANAGEMENT_ENDPOINTS_WEB_BASEPATH=/actuator 1327 | - DATABASE=mysql 1328 | ``` 1329 | 1330 | * The relative paths `../../*` are all relative to this file. Clone the repository to look at those: `git clone https://github.com/dsyer/docker-services` and look at `layers/samples`. 1331 | * Important features: 1332 | * `base`: a generic `Deployment` and `Service` with a `Pod` listening on port 8080 1333 | * `mysql`: a local MySQL `Deployment` and `Service`. Needs a `PersistentVolume` so only works on Kubernetes clusters that have a default volume provider 1334 | * `transformers`: patches to the basic application deployment. The patches are generic and could be shared by multiple different applications. 1335 | * `env-config`: the base layer uses this `ConfigMap` to expose environment variables for the application container. These entries are used to adapt the PetClinic to the Kubernetes environment. 1336 | --------------------------------------------------------------------------------