Use ConfigMaps and Secrets with Oracle Cloud Native Environment
Introduction
A key advantage of containers is knowing your software executes on any computer without problems. Many applications use environment variables to control the application's runtime behavior, such as an API Key or environment variables. They may be hard-coded into a container, but doing so makes switching environments between a Production and Development environment difficult. Kubernetes addresses this by moving the application's configuration into an object called a ConfigMap comprised of a series of key-value pairs injected into the application's environment at runtime. The application then consumes these transparently as though they were environment values in their runtime environment.
Objectives
In this tutorial, you'll learn how to:
- Use ConfigMaps and Secrets
Prerequisites
- Installation of Oracle Cloud Native Environment
- a single control and worker node
Deploy Oracle Cloud Native Environment
Note: If running in your own tenancy, read the linux-virt-labs
GitHub project README.md and complete the prerequisites before deploying the lab environment.
Open a terminal on the Luna Desktop.
Clone the
linux-virt-labs
GitHub project.git clone https://github.com/oracle-devrel/linux-virt-labs.git
Change into the working directory.
cd linux-virt-labs/ocne2
Install the required collections.
ansible-galaxy collection install -r requirements.yml
Deploy the lab environment.
ansible-playbook create_instance.yml -e localhost_python_interpreter="/usr/bin/python3.6" -e install_ocne_rpm=true -e create_ocne_cluster=true -e "ocne_cluster_node_options='-n 1 -w 1'"
The free lab environment requires the extra variable
local_python_interpreter
, which setsansible_python_interpreter
for plays running on localhost. This variable is needed because the environment installs the RPM package for the Oracle Cloud Infrastructure SDK for Python, located under the python3.6 modules.The default deployment shape uses the AMD CPU and Oracle Linux 8. To use an Intel CPU or Oracle Linux 9, add
-e instance_shape="VM.Standard3.Flex"
or-e os_version="9"
to the deployment command.Important: Wait for the playbook to run successfully and reach the pause task. At this stage of the playbook, the installation of Oracle Cloud Native Environment is complete, and the instances are ready. Take note of the previous play, which prints the public and private IP addresses of the nodes it deploys and any other deployment information needed while running the lab.
Overview of ConfigMaps
A ConfigMap stores non-confidential data in key-value pairs, which are subsequently available for any deployed Pods to utilize as environment variables, command-line arguments, or configuration files in a volume. For this reason, an administrator needs to know what they are and how to use them.
Important: Using a ConfigMap does not provide secrecy or encryption. Do not use a ConfigMap for confidential data. Use a Secret or another external solution to keep the data private.
Another restriction to their use is that the ConfigMap file is limited to a maximum size of 1 MB. Larger datasets should use alternate storage methods, such as databases.
Create a ConfigMap
While you create most ConfigMaps using a YAML file with the kubectl create -f
command, creating a ConfigMap directly from kubectl
itself is also possible. Only use direct entry when defining a small number of key-value pairs.
Open a terminal and connect via SSH to the ocne instance.
ssh oracle@<ip_address_of_node>
Use direct entry to define a key-value pair.
kubectl create configmap mytest --from-literal=Country=USA
Where the key-value pair stored in the ConfigMap is Country=USA.
Confirm the creation of the ConfigMap.
kubectl get configmap mytest
Example Output:
NAME DATA AGE mytest 1 22s
Describe the ConfigMap.
kubectl describe configmap mytest
Example Output:
Name: mytest Namespace: default Labels: <none> Annotations: <none> Data ==== Country: ---- USA BinaryData ==== Events: <none>
Enter multiple key-value pairs.
Most ConfigMaps consist of several entries. This example shows how to enter multiple key-value pairs.
kubectl create configmap mytest01 --from-literal=Country=USA --from-literal=State=ME --from-literal=City=Portland
Describe the ConfigMap.
kubectl describe configmap mytest01
Example Output:
Name: mytest01 Namespace: default Labels: <none> Annotations: <none> Data ==== City: ---- Portland Country: ---- USA State: ---- ME BinaryData ==== Events: <none>
Inspect the definition in YAML format.
kubectl get configmap mytest01 -o yaml
Example Output:
apiVersion: v1 data: City: Portland Country: US State: ME kind: ConfigMap metadata: creationTimestamp: "2023-09-20T15:17:24Z" name: mytest02 namespace: default resourceVersion: "23860" uid: b425b856-9b22-4cf4-9daa-9c1f4aea39e7
Check what ConfigMaps are currently defined system-wide.
kubectl get configmap
Example Output:
NAME DATA AGE kube-root-ca.crt 1 5h20m mytest 1 132m mytest01 1 52m
Did you notice the ConfigMap kube-root-ca.crt in the list? This ConfigMap is created as part of the installation process and is used internally by Oracle Cloud Native Environment. It should not be altered or deleted.
Inspect the contents of the kube-root-ca.crt ConfigMap.
kubectl get configmap kube-root-ca.crt -o yaml
Example Output:
apiVersion: v1 data: ca.crt: | -----BEGIN CERTIFICATE----- MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl cm5ldGVzMB4XDTIzMDkyMDEwMzk0N1oXDTMzMDkxNzEwMzk0N1owFTETMBEGA1UE AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKWO PCSCOAjr74K/H8wO+TgGxP6mbiYOvYkkmXpKmd25paO+sOgEsFDpzd2IJrF62wdF XOMPmFuKqBabxLD9BFxnu5NQGCXmQzSKjOxPTy+tIYMIoCSiqy5uNTtOV/2HOOhP a0e1vcYVuCTMlJXzvCh8w90LTDzHH6vk5IanhwbNgVb3L7IyOKeE02foqp7Yfo2t 8KQElJNBjtZediHXWArHTI5m7fuLZy+QzFFDn/lCiwHB64gwNwvvaPJ158q2K49K cw3v222zzo6/RQv3nFCoq45LjdyrjHCbHHic5r2ZZH0oSnA7foKq2j+SGJ9w3KjC 1oiGGzx3BSM3JWl38GUCAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB /wQFMAMBAf8wHQYDVR0OBBYEFM+0ErJ+VJfYmbN5usVZwIK9kPScMBUGA1UdEQQO MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBACAJ5RnQk0Qm771JXSrP U1ksIXO1K5BvZKUOCRuQowgyMWLF2ndFccvyDYwEjRflHC6spDOfgEcWgOtS2aYS QmkMr9W07yfVgZXUxmtSOPmpKLXLgf+auCGaR5LQ70gGEqocqY3KDK0EKdZuj47y WLahJ6PqVtzy6gkwNRz9/EWFbO12zMiHsyzO9wksZO5q3bm27pdOLe0tQpg4RNL4 dx2XVTg2ruvqLypdkQWAis+VHJF72JA8GVjfWGboCd6nYSfdKEzhNlvTqRi/j/uF 7kywrKLX3O0iQYzrwnbFZYY+FaXLTP1Q7jSnNEaqjxrCin2x9zXiVpifjZG5cza2 /yE= -----END CERTIFICATE----- kind: ConfigMap metadata: annotations: kubernetes.io/description: Contains a CA bundle that can be used to verify the kube-apiserver when using internal endpoints such as the internal service IP or kubernetes.default.svc. No other usage is guaranteed across distributions of Kubernetes clusters. creationTimestamp: "2023-09-20T10:40:23Z" name: kube-root-ca.crt namespace: default resourceVersion: "355" uid: 4576026a-0632-4e43-99b4-be199863c84f
Create a ConfigMap from a YAML File
Rather than creating a ConfigMap declaratively from the command line, it is often both easier and faster to define the key-value pairs within a YAML manifest file in advance.
Create the ConfigMap YAML file manifest.
cat << 'EOF' > ~/data-config.yaml apiVersion: v1 kind: ConfigMap metadata: name: data-config data: database_url: jdbc:oracle:thin:@localhost:1521:<SID> user: oracle EOF
Create the ConfigMap.
kubectl apply -f data-config.yaml
Use a ConfigMap as an Environment Variable
A ConfigMap, once declared, can be used by one or more Pods deployed into the same namespace. The key-value pairs of a ConfigMap can be used as an environment variable within the Pod by using the envFrom.configMapRef variable to inject the key-value pairs as environment variables. Next, you will deploy a Pod and inject the ConfigMap's key-value pairs you just created into the Pods environment.
Create the Pod definition.
cat << 'EOF' > ~/web-pod.yaml apiVersion: v1 kind: Pod metadata: name: web-pod spec: containers: - image: ghcr.io/oracle/oraclelinux9-nginx:1.20 name: app envFrom: - configMapRef: name: data-config EOF
Look at the bottom of the file to see the envFrom.configMapRef variable.
Create the deployment.
kubectl apply -f web-pod.yaml
Inspect the Pod's environment to confirm that environment variables are present.
kubectl exec web-pod -- env
If the output does not show a similar output as the sample output below, you may need to retry running this command until it works.
Example Output:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TERM=xterm HOSTNAME=web-pod database_url=jdbc:oracle:thin:@localhost:1521:<SID> user=oracle KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 KUBERNETES_PORT_443_TCP_PROTO=tcp KUBERNETES_PORT_443_TCP_PORT=443 KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 KUBERNETES_SERVICE_HOST=10.96.0.1 KUBERNETES_SERVICE_PORT=443 KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_PORT=tcp://10.96.0.1:443 NGINX_VERSION=1.25.0 NJS_VERSION=0.4.1 PKG_RELEASE=1~buster HOME=/root
The output indicates that Kubernetes successfully injected the database_url and user ConfigMap variables into the Pod's environment variables.
However, this does not match the naming convention used by the Pod's other environment variables, which is to capitalize variables. You could modify the original ConfigMap YAML definition file - which might impact other running services. Or you could redefine the keys used to inject the ConfigMap key-value pairs into the Pod by using the
valueFrom
attribute in the Pod's definition YAML file.Delete the original Pod deployment.
kubectl delete -f web-pod.yaml
Create the Pod definition.
cat << 'EOF' > ~/web-pod01.yaml apiVersion: v1 kind: Pod metadata: name: web-pod spec: containers: - image: ghcr.io/oracle/oraclelinux9-nginx:1.20 name: app env: - name: DATABASE_URL valueFrom: configMapKeyRef: name: data-config key: database_url - name: USERNAME valueFrom: configMapKeyRef: name: data-config key: user EOF
Create the deployment.
kubectl apply -f web-pod01.yaml
Confirm the environment variables now match the typical conventions used for environment variables in this Pod's environment.
kubectl exec web-pod -- env
Example Output:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TERM=xterm HOSTNAME=web-pod DATABASE_URL=jdbc:oracle:thin:@localhost:1521:<SID> USER=oracle KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 KUBERNETES_PORT_443_TCP_PROTO=tcp KUBERNETES_PORT_443_TCP_PORT=443 KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 KUBERNETES_SERVICE_HOST=10.96.0.1 KUBERNETES_SERVICE_PORT=443 KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_PORT=tcp://10.96.0.1:443 NGINX_VERSION=1.25.0 NJS_VERSION=0.4.1 PKG_RELEASE=1~buster HOME=/root
The output shows the expected capitalization of the DATABASE_URL and USER environment variables.
Mount a ConfigMap as a Volume
A ConfigMap mounted as a volume is useful when processing larger datasets of key-value pairs. This method allows the application to read the key-value pairs from the filesystem.
Delete the original Pod deployment.
kubectl delete -f web-pod01.yaml
Create a new Pod definition.
cat << 'EOF' > ~/nginx-pod.yaml apiVersion: v1 kind: Pod metadata: name: nginx-pod spec: containers: - image: ghcr.io/oracle/oraclelinux9-nginx:1.20 name: app volumeMounts: - name: foo mountPath: /etc/config readOnly: true volumes: - name: foo configMap: name: data-config EOF
Create the deployment.
kubectl apply -f nginx-pod.yaml
Open an interactive shell to confirm the environment variables now match the typical conventions used for environment variables.
kubectl exec -it nginx-pod -- /bin/sh
Check the expected ConfigMap keys are showing.
This step uses the mount path (
/etc/config
) defined in the Pod deployment descriptor and lists the keys that appear as files.ls -1 /etc/config
Example Output:
# ls -1 /etc/config database_url user
Confirm each file's contents correspond to the value defined in the ConfigMap.
Check the database_url key contains the expected value.
cat /etc/config/database_url; echo
Check the user key contains the expected value.
cat /etc/config/user; echo
Exit the Pod's interactive shell.
exit
That confirms the Pod mounts the ConfigMap as a volume.
Edit a ConfigMap
One drawback of using environment variables or command-line parameters with Pods is the inability to update them while running the application. However, using a volume-based ConfigMap allows you to update the configuration values without recreating or restarting the Pod. So, when you update the underlying Configmap, all mounted volumes referencing the ConfigMap are also updated.
Important: It can take a few seconds to several minutes after updating a ConfigMap mounted as a volume for the updated values to be accessible within the running application.
Edit the mounted ConfigMap.
kubectl edit configmap data-config
Example Output:
# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 data: database_url: jdbc:oracle:thin:@localhost:1521:<SID> user: oracle kind: ConfigMap metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"v1","data":{"database_url":"jdbc:oracle:thin:@localhost:1521:\u003cSID\u003e","user":"oracle"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"backend-config","namespace":"default"}} creationTimestamp: "2023-09-21T12:52:07Z" name: backend-config namespace: default resourceVersion: "40737" uid: 7b01dab8-f985-4d05-8e87-51babb412614 ~ ~ ~ ~ "/tmp/kubectl-edit-2548943921.yaml" 18L, 762C
The command opens the default editor for the system, which in Oracle Linux is
vi
.Update the user: oracle key-value pair.
Enter edit mode by pressing the
i
key.Navigate to the user: oracle key-value pair.
Change the value of
oracle
toelcaro
.Save and exit the
vi
editor by entering:wq!
and then pressingEnter
.
Open an interactive shell to confirm the environment variables now match the typical conventions used for environment variables.
kubectl exec -it nginx-pod -- /bin/sh
Confirm that the user key-value pair just edited shows the new value entered into the ConfigMap.
cat /etc/config/user; echo
If the expected value is absent, keep checking, as the new value may not appear for several minutes. You can use
watch cat /etc/config/user
to monitor the change andCtrl-c
to exit out.Exit the Pod's interactive shell.
exit
That confirms the ConfigMap's
user
key-value pair has been updated and is accessible to the Pod using it.
Look at Kubernetes Secrets
Kubernetes Secrets, like ConfigMaps, are designed to store information in a key-value pair using the same sources.
However, Secrets are intended to store confidential information, albeit using a basic level of security because they are Base64 encoded, which makes them easily decoded by a malicious attacker. Explaining how to improve the encryption provided by Kubernetes Secrets is beyond the scope of this tutorial but will be covered sometime in the future. For now, let's look at the simplest form of Kubernetes Secrets.
Create a Secret
You create Kubernetes Secrets using the same methods you used to generate ConfigMaps, so the process will be familiar.
Create a Secret using a literal value.
kubectl create secret generic test --from-literal=pwd=P@ssw0rd
Confirm the Secret contains the value entered.
kubectl describe secret test
Example Output:
Name: test Namespace: default Labels: <none> Annotations: <none> Type: Opaque Data ==== pwd: 8 bytes
The Secret is created but shows Type: Opaque rather than what is stored. What does that mean? This default Secret type is assigned when you do not provide a specific type in a Secret Configuration file.
Confirm the Secret has stored the value entered.
kubectl get secret test -o yaml
Example Output:
apiVersion: v1 data: pwd: UEBzc3cwcmQ= kind: Secret metadata: creationTimestamp: "2023-09-22T12:12:02Z" name: test namespace: default resourceVersion: "5872" uid: 59526821-faff-414c-956a-23f308990115 type: Opaque
Notice the difference in how the key-value pair is displayed. This difference is because the contents of the Secret's values are Base64-encoded, whereas those stored in a ConfigMap are in clear text. The next step shows how to decode the stored value to confirm it matches the entered value.
Decode the Base64-encoded value stored in the newly created Secret.
echo -n 'UEBzc3cwcmQ=' | base64 --decode; echo
The output confirms the stored value matches the entered value.
Create a Secret from a YAML File
Like a ConfigMap, creating a Secret declaratively from a YAML file is easier and faster.
Base64-encode the value.
Creating a Secret entails a slight catch because you must Base64-encode the value to be stored yourself. While there are numerous websites you could use, it is also possible to do this yourself from the command line.
echo -n 'P@ssw0rd' | base64
The output returns a value that is base64-encoded, and you'll use that in the YAML file.
Create the Secret YAML file.
cat << 'EOF' > ~/password.yaml apiVersion: v1 kind: Secret metadata: name: password type: Opaque data: pwd: UEBzc3cwcmQ= EOF
Create the Secret.
kubectl apply -f password.yaml
Confirm the Secret gets created as expected.
kubectl get secret password -o yaml
Example Output:
apiVersion: v1 data: pwd: UEBzc3cwcmQ= kind: Secret metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"v1","data":{"pwd":"UEBzc3cwcmQ="},"kind":"Secret","metadata":{"annotations":{},"name":"password","namespace":"default"} "type":"Opaque"} creationTimestamp: "2023-09-22T13:01:53Z" name: password namespace: default resourceVersion: "10061" uid: b02c90a6-11e5-48c4-acd9-20c007696176 type: Opaque
Use a Secret as an Environment Variable
Using the key-value value pairs defined within a Secret is almost the same as using a ConfigMap. A difference is that instead of using the envFrom.ConfigMapRef
path, you have to use the envFrom.secretRef
instead.
Create the Pod definition.
cat << 'EOF' > ~/password-pod.yaml apiVersion: v1 kind: Pod metadata: name: password-pod spec: containers: - image: ghcr.io/oracle/oraclelinux9-nginx:1.20 name: app envFrom: - secretRef: name: password EOF
Create the deployment.
kubectl apply -f password-pod.yaml
Inspect the Pod's environment for the injection of the variable.
kubectl exec password-pod -- env
Example Output:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TERM=xterm HOSTNAME=password-pod pwd=P@ssw0rd KUBERNETES_PORT=tcp://10.96.0.1:443 KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 KUBERNETES_PORT_443_TCP_PROTO=tcp KUBERNETES_PORT_443_TCP_PORT=443 KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 KUBERNETES_SERVICE_HOST=10.96.0.1 KUBERNETES_SERVICE_PORT=443 KUBERNETES_SERVICE_PORT_HTTPS=443 NGINX_VERSION=1.25.0 NJS_VERSION=0.7.12 PKG_RELEASE=1~bullseye HOME=/root
You should see the pwd=P@ssw0rd Secret as one of the Pod's environment variables.
Mount a Secret as a Volume
Mounting Secrets as Volumes is quite a common practice because it makes it easy to make them available to a Pod at runtime. This process is almost identical to that used to create a ConfigMap, except that the attribute used is different in Secrets. With Secrets, you need to reference the secrets.secretName attribute.
Delete the original Pod deployment.
kubectl delete -f password-pod.yaml
Create the Pod definition.
cat << 'EOF' > ~/password-pod01.yaml apiVersion: v1 kind: Pod metadata: name: secret-pod spec: containers: - image: ghcr.io/oracle/oraclelinux9-nginx:1.20 name: app volumeMounts: - name: secret-volume mountPath: /var/app readOnly: true volumes: - name: secret-volume secret: secretName: password EOF
Create the deployment.
kubectl apply -f password-pod01.yaml
Open an interactive shell to confirm the Secret lists as a file with the Pod's environment.
kubectl exec -it secret-pod -- /bin/sh
Check that the expected Secret key is showing.
ls /var/app
Confirm the file's contents correspond to the value defined in the ConfigMap.
cat /var/app/pwd; echo
The
pwd
Secret key should contain the Base64 value ofP@ssw0rd#
.Exit the Pod's interactive shell.
exit
Next Steps
This concludes the walkthrough introducing Kubernetes ConfigMaps and Secrets and showing how to use them in application deployments.