A demo Spring Boot application that demonstrates on using Kubernetes Secrets to configure Spring Boot application.
-
You might need an access to Kubernetes Cluster to play with this application. The easiest way to get local Kuberentes cluster up and running is using minikube
-
Install Spring Boot CLI
-
Untar/Unzip Spring Boot CLI 1.5.7.RELEASE and add it your path
Note
|
The rest of the document assumes you have minikube up and running |
For the demo purpose we will be creating a simple Spring Boot project,
spring init --artifactId=spring-boot-secrets-demo \
--name="Kubernetes:: Spring Boot:: Demos :: Secrets Demo" \
--groupId="com.redhat.developers" \
--package-name="com.redhat.developers" \
--dependencies=web,actuator,security,lombok \
--extract spring-boot-secrets-demo (1)
cd spring-boot-secrets-demo
./mvnw io.fabric8:fabric8-maven-plugin:3.5.30:setup (2)
-
Creates a Spring Boot project with web and actuator dependencies
-
Adds the fabric8-maven-plugin to the project that will help with deploying the application on Kubernetes
The complete demo sources is available here
The directory spring-boot-secrets-demo will be referred to as $PROJECT_HOME throughout this document.
The spring-boot-secrets-demo application uses spring-security which by default enables security on the entire spring boot application.
The default user and password is displayed something like Using default security password: 981d5f9f-c8ea-413f-8f3b-71daaa20d53c
when the spring boot application boots up.
To demonstrate how Kubernetes secrets can be used to override the default user/password, update the application.properties
to be like:
security.user.name=${SECRETS_DEMO_USER:demo} (1)
security.user.password=${SECRETS_DEMO_USER_PASSWD:demo} (2)
-
The spring security user
-
The spring security password
Create kubernetes secrets called spring-security (this just a name I am using but it could be anything of your choice) and add two properties like spring.user.name and spring.user.password to it.
kubectl create secret generic spring-security \
--from-literal=spring.user.name=demo \
--from-literal=spring.user.password=password (1)
kubectl get secret spring-security -o yaml (2)
-
creates a kubernetes secret called
spring-security
with to properties calledspring.user.name
andspring.user.password
-
Displays the secret spring-security content
Example output of kubectl get secret spring-security -o yaml
apiVersion: v1
data:
spring.user.name: ZGVtbw== (1)
spring.user.password: cGFzc3dvcmQ= (1)
kind: Secret
metadata:
creationTimestamp: 2017-09-19T15:24:29Z
name: spring-security
namespace: default
resourceVersion: "71363"
selfLink: /api/v1/namespaces/default/secrets/spring-security
uid: a0e0254e-9d4e-11e7-9b8d-080027da6995
type: Opaque
-
Displays the value of the secret as base64 encoded string
As the application is configured to use fabric8-maven-plugin, we can create Kubernetes deployment
and service as fragments in $PROJECT_HOME/src/main/fabric8
. The fabric8-maven-plugin takes
care of building the complete Kubernetes manifests by merging the contents of the fragment(s) from $PROJECT_HOME/src/main/fabric8
during deploy.
Create a file called deployment.yaml in $PROJECT_HOME/src/main/fabric8
with the following contents,
spec:
template:
spec:
containers:
- env:
- name: SECRETS_DEMO_USER (1)
valueFrom:
secretKeyRef: (2)
name: spring-security (3)
key: spring.user.name (4)
- name: SECRETS_DEMO_USER_PASSWD (1)
valueFrom:
secretKeyRef: (2)
name: spring-security (3)
key: spring.user.password (4)
-
Define Environment variable called SECRETS_DEMO_USER and SECRETS_DEMO_USER_PASSWD
-
Defines that the value will be a Secret reference
-
Name of the Secret from where to look for the key/value pair, this should be same name you have used in Create secret
-
The property key inside the Secret, whose value needs to be assigned to environment variable SECRETS_DEMO_USER and SECRETS_DEMO_USER_PASSWD respectively
To enable easy access of the application, we make the type of Kubernetes service as NodePort,
create a file called svc.yaml in $PROJECT_HOME/src/main/fabric8
with the following contents,
apiVersion: v1
kind: Service
spec:
type: NodePort (1)
-
expose the service using NodePort
To deploy the application execute the command ./mvnw clean fabric8:deploy
. The application deployment status can be
checked using the command kubectl get pods -w
To access and test the application execute the following command,
curl -i $(minikube service spring-boot-secrets-demo --url)/; echo ""; (1)
curl -u demo:password $(minikube service spring-boot-secrets-demo --url)/mygithuborgs; echo ""; (2)
-
Should fail with unauthorized (HTTP 401) error
-
Should still display 404 - as there is no resource at / but your request is now authorized
The above command should display a message like Hello jerry! Welcome to Configuring Spring Boot on Kubernetes!
Note
|
minikube service spring-boot-secrets-demo --url is used to get the service url and port via which we can access
the application
|
The secrets could also be mounted as files inside the containers. To demostrate lets build a simple REST controller called GitHubController, that will query the GitHub API for your organizations list. Querying for organizations list is a authorized call hence you need to create your Personal Access Token
Store the your github user id in a file called github.user
and the Personal Access Token
in a file called github.token
.
The secrets can also be created from file, we will use github.user
and github.token
to create a new secret called
spring-github-demo
kubectl create secret generic spring-github-demo \
--from-file ./github.user \ (1)
--from-file ./github.token (2)
kubectl get secret spring-github-demo -oyaml (3)
-
Adds file
github.user
to secret -
Adds file
github.token
to secret -
Display the created secret
Example output from kubectl get secret spring-github-demo -oyaml
apiVersion: v1
data:
github.token: NzIyMzUxMjMwMDlmYjYyMzNkMTAzZTlmMjVmMGIzOTQyNWRiOTc5Zgo= (1)
github.user: a2FtZXNoc2FtcGF0aAo= (2)
kind: Secret
metadata:
creationTimestamp: 2017-09-19T16:27:10Z
name: spring-github-demo
namespace: default
resourceVersion: "75673"
selfLink: /api/v1/namespaces/default/secrets/spring-github-demo
uid: 632e05fd-9d57-11e7-9b8d-080027da6995
type: Opaque
-
File
./github.token
stored as base64 encoded string -
File
./github.user
stored as base64 encoded string
security.user.name=${SECRETS_DEMO_USER:demo}
security.user.password=${SECRETS_DEMO_USER_PASSWD:demo}
demo.secretsPath=/deployments (1)
-
The secret files mount path
The deployment.yaml needs to be updated to mount the user and token as files, update the deployment.yaml in $PROJECT_HOME/src/main/fabric8 with the following contents,
spec:
template:
spec:
containers:
- env:
- name: SECRETS_DEMO_USER
valueFrom:
secretKeyRef:
name: spring-security
key: spring.user.name
- name: SECRETS_DEMO_USER_PASSWD
valueFrom:
secretKeyRef:
name: spring-security
key: spring.user.password
volumeMounts:
- name: github-user (1)
mountPath: "/deployments/github" (2)
readOnly: true
volumes:
- name: github-user
secret:
secretName: spring-github-demo (3)
items:
- key: github.user (4)
path: user (5)
- key: github.token (6)
path: token (7)
-
Logical name of the volume to refer later in the deployment
-
The path or location within container where to mount the files
-
The secret name from which to load the secrets
-
The key github.user from secret spring-github-demo
-
The path under /deployments/github, in this case /deployments/github/user
-
The key github.token from secret spring-github-demo
-
The path under /deployments/github, in this case /deployments/github/token
package com.redhat.developers;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class KubernetesSpringBootDemosSecretsDemoApplication {
public static void main(String[] args) {
SpringApplication.run(KubernetesSpringBootDemosSecretsDemoApplication.class, args);
}
@Configuration
public class DemoConfiguration {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { (1)
return restTemplateBuilder.build();
}
}
}
-
Adding RestTemplate that will be used to query GitHub REST API
Create a Controller called GitHubController that will query the GitHub API using your github user id and token.
package com.redhat.developers;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriTemplate;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
/**
* A simple demo rest controller that calls GitHub API to perform some simple operations which will require authorizations
* The idea of this controller is to demonstrate how to use the Kubernetes Secrets mounted as file
* and use the token and username to call GitHub
*
* @author kameshsampath
*/
@Slf4j
@RestController
public class GitHubController {
@Value("${demo.secretsPath}")
private String secretsPath; //(1)
private final RestTemplate restTemplate;
public GitHubController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
/**
* Get the list of organizations that the user belongs to in GitHub
*/
@GetMapping("/mygithuborgs")
public ResponseEntity<String> listMyOrganizations() {
try {
final URI githubUserSecretsURI = ResourceUtils.getURL(secretsPath + "/github/user").toURI(); // (2)
final URI githubUserTokenSecretsURI = ResourceUtils.getURL(secretsPath + "/github/token").toURI(); //(3)
final byte[] encodedGithubUser = Files.readAllBytes(Paths.get(githubUserSecretsURI));
final byte[] encodedGithubToken = Files.readAllBytes(Paths.get(githubUserTokenSecretsURI));
String githubUser = sanitize(encodedGithubUser);
String githubUserToken = sanitize(encodedGithubToken);
String authHeader = String.format("%s:%s", githubUser, githubUserToken);
log.info("Listing Organizations of user :{}", githubUser);
String basicAutheader = Base64.getEncoder().encodeToString(authHeader.getBytes());
log.info("Auth Header : {}", basicAutheader);
ResponseEntity<String> response =
restTemplate.exchange(buildHttpEntity("user/orgs", basicAutheader), String.class);
return ResponseEntity
.status(response.getStatusCode().value())
.body(response.getBody());
} catch (URISyntaxException e) {
log.error("Error querying github", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
} catch (IOException e) {
log.error("Error querying github", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}
/**
* A method to build {@link RequestEntity} by adding needed basic authentication headers
*
* @param path - the github api path to call with out leading "/"
* @param basicAutheader - the Basic Authorization Base64 string representation header value
* @return {@link RequestEntity}
*/
private RequestEntity<Void> buildHttpEntity(String path, String basicAutheader) {
URI githubApiUri = new UriTemplate("https://api.github.com/{path}").expand(path);
log.info("Calling API:{}", githubApiUri.toASCIIString());
RequestEntity<Void> requestEntity =
RequestEntity.get(githubApiUri)
.header("Authorization", String.format(" Basic %s", basicAutheader))
.accept(MediaType.parseMediaType("application/vnd.github.v3+json"))
.build();
return requestEntity;
}
/**
* remove all new lines from the String
*
* @param strBytes - the string bytes where newline to be removed
* @return sanitized string without newlines
*/
private String sanitize(byte[] strBytes) {
return new String(strBytes)
.replace("\r", "")
.replace("\n", "");
}
}
-
The secrets path where to look for the file
-
The github user secret path mounted inside the container
-
The github user token path mounted inside the container
The application can be Deployed like before.
To access and test the application execute the following command,
curl -i $(minikube service spring-boot-secrets-demo --url)/mygithuborgs; echo ""; (1)
curl -u demo:password $(minikube service spring-boot-secrets-demo --url)/mygithuborgs; echo ""; (2)
-
Should fail with unauthorized (HTTP 401) error
-
Should display your github organizations
--END--