Use ConfigMaps and Secrets with Oracle Cloud Native Environment

0
0
Send lab feedback

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.

  1. Open a terminal on the Luna Desktop.

  2. Clone the linux-virt-labs GitHub project.

    git clone https://github.com/oracle-devrel/linux-virt-labs.git
  3. Change into the working directory.

    cd linux-virt-labs/ocne2
  4. Install the required collections.

    ansible-galaxy collection install -r requirements.yml
  5. 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 sets ansible_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.

  1. Open a terminal and connect via SSH to the ocne instance.

    ssh oracle@<ip_address_of_node>
  2. 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.

  3. Confirm the creation of the ConfigMap.

    kubectl get configmap mytest

    Example Output:

    NAME     DATA   AGE
    mytest   1      22s
  4. Describe the ConfigMap.

    kubectl describe configmap mytest

    Example Output:

    Name:         mytest
    Namespace:    default
    Labels:       <none>
    Annotations:  <none>
    Data
    ====
    Country:
    ----
    USA
    
    BinaryData
    ====
    
    Events:  <none> 
  5. 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
  6. 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>
  7. 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
  8. 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.

  9. 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.

  1. 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
  2. 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.

  1. 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.

  2. Create the deployment.

    kubectl apply -f web-pod.yaml
  3. 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.

  4. Delete the original Pod deployment.

    kubectl delete -f web-pod.yaml
  5. 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
  6. Create the deployment.

    kubectl apply -f web-pod01.yaml
  7. 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.

  1. Delete the original Pod deployment.

    kubectl delete -f web-pod01.yaml
  2. 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
  3. Create the deployment.

    kubectl apply -f nginx-pod.yaml
  4. 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
  5. 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
  6. Confirm each file's contents correspond to the value defined in the ConfigMap.

    1. Check the database_url key contains the expected value.

      cat /etc/config/database_url; echo
    2. Check the user key contains the expected value.

      cat /etc/config/user; echo
  7. 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.

  1. 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.

  2. Update the user: oracle key-value pair.

    1. Enter edit mode by pressing the i key.

    2. Navigate to the user: oracle key-value pair.

    3. Change the value of oracle to elcaro.

    4. Save and exit the vi editor by entering :wq! and then pressing Enter.

  3. 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
  4. 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 and Ctrl-c to exit out.

  5. 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.

  1. Create a Secret using a literal value.

    kubectl create secret generic test --from-literal=pwd=P@ssw0rd
  2. 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.

  3. 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.

  4. 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.

  1. 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.

  2. Create the Secret YAML file.

    cat << 'EOF' > ~/password.yaml
    apiVersion: v1
    kind: Secret
    metadata:
      name: password
    type: Opaque
    data:
       pwd: UEBzc3cwcmQ=
    EOF
    
  3. Create the Secret.

    kubectl apply -f password.yaml
  4. 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.

  1. 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
  2. Create the deployment.

    kubectl apply -f password-pod.yaml
  3. 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.

  1. Delete the original Pod deployment.

    kubectl delete -f password-pod.yaml
  2. 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
  3. Create the deployment.

    kubectl apply -f password-pod01.yaml
  4. Open an interactive shell to confirm the Secret lists as a file with the Pod's environment.

    kubectl exec -it secret-pod -- /bin/sh
  5. Check that the expected Secret key is showing.

    ls /var/app
  6. 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 of P@ssw0rd#.

  7. 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.

SSR