Kubernetes memiliki fitur yang bernama ConfigMap yang biasa digunakan untuk menyimpan konfigurasi suatu aplikasi. Di artikel ini kita akan membahas tentang ConfigMap dan kita bisa pakai ConfigMap untuk apa saja.

Apa Itu ConfigMap?

ConfigMap allows you to decouple configuration artifacts from image content to keep containerized applications portable.

Menurut penjelasan resmi dari dokumentasi Kubernetes, ConfigMap adalah sebuah mekanisme untuk memisahkan konfigurasi dari sebuah aplikasi yang sudah dikontainerkan. Hal ini dapat membuat aplikasi tersebut lebih portabel dan membuat konfigurasinya lebih mudah untuk diubah dan dimanage, dan juga mengurangi kemungkinan konfigurasi yang langsung ditulis ke dalam sebuah Pod. ConfigMap cocok digunakan untuk menyimpan konfigurasi yang tidak sensitif atau konfigurasi yang tidak perlu dienkripsi, seperti variabel lingkungan.

Bagaimana Cara Kerja ConfigMap?

Gambar 1.1 — Membuat ConfigMap Gambar 1.1 — Membuat ConfigMap

Pada Gambar 1.1 kita membuat ConfigMap terlebih dahulu. Ada 3 cara untuk membuat ConfigMap:

  1. Menggunakan opsi –from-file. Contoh: kubectl create configmap myConfigMap --from-file /my/path/to/directory/or/file
  2. Menggunakan opsi –from-literal. Contoh: kubectl create configmap myConfigMap --from-literal KEY1=VALUE1 KEY2=VALUE2
  3. Menggunakan manifest dengan tipe ConfigMap (lihat gambar 2.1). Untuk menggunakan cara ini kita perlu membuat sebuah file dengan ekstensi .yaml dan sesuai dengan spesifikasi, lalu mengunggah file tersebut dengan perintah kubectl apply -f myConfigMap.yaml
Gambar 1.2 — Unggah ConfigMap Gambar 1.2 — Unggah ConfigMap

Perintah kubectl create configmap maupun kubectl apply -f <file> akan mengunggah ConfigMap tersebut ke Apiserver Kubernetes yang ada di dalam cluster Kubernetes.

Gambar 1.2 — Apiserver mendistribusikan ConfigMap Gambar 1.2 — Apiserver mendistribusikan ConfigMap

Lalu apiserver akan mendistribusikan ConfigMap tersebut ke semua Pod yang membutuhkan yang berada di dalam cluster Kubernetes.

Bagaimana Cara Menggunakan ConfigMap?

Ada 2 cara untuk menggunakan ConfigMap:

1. Sebagai environment variable

Sebagai contoh, kita memiliki sebuah ConfigMap seperti berikut

Gambar 2.1 - ConfigMap Gambar 2.1 - ConfigMap

Penjelasan nomor pada gambar:

    1. Nama ConfigMap
    2. Blok data untuk sebuah ConfigMap

Lalu kita gunakan nilai dari ConfigMap “special-config” (dari key “metadata.name”) tersebut ke sebuah Pod dengan menggunakan env seperti berikut

Gambar 2.2 - Konfigurasi Pod Gambar 2.2 - Konfigurasi Pod

Penjelasan nomor pada gambar:

    1. Nama environment variable yang akan digunakan oleh aplikasi
    2. Referensi ke ConfigMap yang akan digunakan. Dalam contoh ini mengacu kepada ConfigMap di gambar 2.1
    3. Key dari sebuah ConfigMap yang akan dipakai nilainya
    4. Referensi ke ConfigMap lain tanpa harus mendefinisikan key yang akan digunakan

2. Sebagai volume yang dipasang ke sebuah pod

ConfigMap juga dapat digunakan dengan menggunakan plugin volume. Kita ambil contoh dengan menggunakan konfigurasi ConfigMap yang sama dengan di atas (gambar 2.1). Kita buat konfigurasi Pod seperti berikut:

Gambar 2.3 - Konfigurasi Pod menggunakan plugin volume Gambar 2.3 - Konfigurasi Pod menggunakan plugin volume

Penjelasan nomor pada gambar:

    1. Referensi ke nama volume yang akan digunakan. Dalam contoh ini nomor 1 akan menggunakan volume dari nomor 3, yaitu volume “config-volume”
    2. Path yang akan digunakan oleh volume tersebut di dalam Pod
    3. Nama volume
    4. Nama ConfigMap yang dijadikan referensi oleh volume “config-volume” (nomor 3)


Menggunakan ConfigMap sebagai variabel lingkungan dengan menggunakan volume membuka peluang untuk mengubah variabel lingkungan yang ada didalam sebuah container atau Pod tanpa harus melakukan restart. Metode seperti ini biasa disebut dengan “live-update” atau “hot config”.

Kenapa “live-update” dibutuhkan? Karena di dalam dunia kontainerisasi, jika kita setel variabel lingkungan dengan docker run -e <KEY>=<VALUE> atau menggunakan env pada konfigurasi Pod, kita tidak bisa langsung menggantinya pada saat aplikasi berjalan. Sehingga kita perlu cara lain untuk membaca variabel lingkungan, yaitu dengan membaca dari sebuah file. Dalam konteks kali ini akan kita menggunakan ConfigMap yang sudah dipasang ke sebuah Pod.

Sebelum kita mulai, mari kita lihat apa yang terjadi jika ConfigMap berubah dengan contoh konfigurasi ConfigMap seperti berikut

Gambar 3.1 - Konfigurasi ConfigMap Gambar 3.1 - Konfigurasi ConfigMap
Gambar 3.2 - ConfigMap di dalam sebuah container sebelum berubah Gambar 3.2 - ConfigMap di dalam sebuah container sebelum berubah

Jika kita perhatikan, setiap variabel yang kita definisikan di dalam ConfigMap akan menjadi sebuah file di dalam kontainer yang ada di dalam setiap Pod. File-file ini memiliki tautan (symlink) ke dalam sebuah folder ..data dengan nama file yang sama. Misal ENV_IS_MAINTENANCE memiliki tautan ke file ..data/ENV_IS_MAINTENACE, dst. Dan folder ..data mengarah kepada sebuah folder yang memiliki nama ..2019_02_22_04_00_47.246998214.

Sekarang mari kita coba membuat sebuah aplikasi sederhana menggunakan NodeJS dan ExpressJS yang membaca variabel lingkungan dari ConfigMap, lalu kita kemas ke dalam sebuah kontainer supaya kita bisa memvalidasi ide di atas.

index.js
  • js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// @ts-check

const express = require("express");
const app = express();
const fs = require("fs");
const bodyParser = require('body-parser');

// volume mount path
const mountPath = "/etc/config";

// ConfigMap filename
const configMapFile = "..data";

// watching file
fs.watch(`${mountPath}`, (event, filename) => {
  // only listen to event "rename".
  // kubelet will rename the reference folder on ConfigMap update.
  if (event === "rename" && filename) {
  if (filename === configMapFile) {
  console.log(`process.env BEFORE: ${JSON.stringify(process.env)}`);

      // get all files from `..data` directory
      const dir = fs.readdirSync(`${mountPath}/${configMapFile}`);

      console.log(`Env list: ${dir}`);

      // read all files inside `..data` directory
      dir.forEach(env => {
          // inject the new envar value to `process.env` object (not recommended)
          process.env[env] = fs.readFileSync(
          `${mountPath}/${configMapFile}/${env}`
          );
      });

      console.log(`process.env AFTER: ${JSON.stringify(process.env)}`);
      }
  }
});

// for readinessProbe and livelinessProbe Kubernetes
app.get("/info", (req, res) => {
res.sendStatus(200);
});

app.get("/isMaintenance", (req, res) => {
const isMaintenace = process.env.ENV_IS_MAINTENANCE;

res.status(200).send(`Is it maintenance? ${isMaintenace}`);
})

app.listen(3000, err => {
if (err) {
console.error(err);
process.exit(1);
}

console.log(`Server is up on port 3000`);
});

Kode di atas akan melihat dan melakukan perubahan jika terjadi event “rename” di dalam folder /etc/config. Setelah itu kita buat sebuah konfigurasi Deployment dan Service untuk sebuah Pod seperti berikut, lalu kita unggah ke Kubernetes dengan kubectl apply -f test-k8s.yaml.

test-k8s.yaml
  • yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
apiVersion: v1
kind: Service
metadata:
  labels:
    test-app: test
  name: test
  namespace: default
spec:
  externalTrafficPolicy: Cluster
  ports:
  - nodePort: 30321
    port: 3000
    protocol: TCP
    targetPort: 3000
  selector:
    test-app: test
  sessionAffinity: None
  type: NodePort
status:
  loadBalancer: {}
---
apiVersion: extensions/v1
kind: Deployment
metadata:
  name: test
spec:
  revisionHistoryLimit: 2
  strategy:
    type: RollingUpdate
  replicas: 1
  selector:
    matchLabels:
      test-app: test
  template:
    metadata:
      labels:
        test-app: test
    spec:
      volumes:
        - name: config-volume
          configMap:
            name: test-config
      containers:
        - name: test
          image: reyhan/docker-try-config:1.0.0
          command: ["npm", "start"]
          imagePullPolicy: Never
          livenessProbe:
            httpGet:
              path: /info
              port: 3000
            initialDelaySeconds: 90
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /info
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
          ports:
            - containerPort: 3000
          volumeMounts:
            - name: config-volume
              mountPath: /etc/config
          resources:
            requests:
              memory: "256Mi"
              cpu: "64m"
            limits:
              memory: "512Mi"
              cpu: "256m"
          env:
            - name: ENV_SQL_CLIENT
              value: "mysql"
            - name: ENV_SQL_HOST
              value: "13.67.44.182"
            - name: ENV_IS_MAINTENANCE
              valueFrom:
                configMapKeyRef:
                  name: test-config
                  key: ENV_IS_MAINTENANCE
            - name: ENV_IS_BLOWN_UP
              valueFrom:
                configMapKeyRef:
                  name: test-config
                  key: ENV_IS_BLOWN_UP

Selanjutnya mari kita ubah konfigurasi ConfigMap di atas menjadi

Gambar 3.3 — Perubahan konfigurasi ConfigMap pada key "ENV_IS_MAINTENTANCE" Gambar 3.3 — Perubahan konfigurasi ConfigMap pada key "ENV_IS_MAINTENTANCE"
Gambar 3.4 — ConfigMap di dalam sebuah container setelah berubah Gambar 3.4 — ConfigMap di dalam sebuah container setelah berubah

Dari gambar di atas dapat kita lihat bahwa tautan antara file-file variabel lingkungan tidak berubah sama sekali. Hanya tautan folder ..data yang berubah tautannya menuju folder baru yang namanya ..2019_02_23_19_01_09.591362024.

Dari animasi di atas dapat kita lihat bagaimana mudahnya mengubah variabel lingkungan tanpa harus mengubah konfigurasi Deployment, Service, atau Pod sama sekali bahkan tanpa harus restart aplikasi tersebut.

Beberapa hal yang harus kita ingat jika menggunakan ConfigMap:

  1. ConfigMap hanya “hidup” di dalam satu namespace saja. Yang artinya kita tidak bisa menggunakan ConfigMap yang berada di namespace lain.
  2. Proses sinkronisasi ConfigMap tidak langsung terjadi atau biasa disebut “eventually consistent”. Hal ini terjadi karena frekuensi sinkronisasi dari kubelet standardnya adalah 60 detik. Jika ingin proses sinkronisasi lebih cepat dapat menggunakan opsi --sync-frequency. Untuk lebih jelasnya bisa baca dari dokumentasi resmi Kubernetes di sini

Ekstra!

Jika kita amati, implementasi “live-update” di atas hanya bisa bekerja untuk NodeJS saja karena NodeJS memiliki pustaka standar untuk sistem file yang bisa melihat perubahan suatu file (dengan menggunakan fs.watch). Apabila kita ingin implementasi di bahasa lain, maka kode server dan konfigurasi Deployment, Service dan Pod di atas perlu sedikit penyesuaian.

k8s.yaml
  • yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
apiVersion: v1
kind: Service
metadata:
  labels:
    test-app: test
  name: test
  namespace: default
spec:
  externalTrafficPolicy: Cluster
  ports:
  - nodePort: 30321
    port: 3000
    protocol: TCP
    targetPort: 3000
  selector:
    test-app: test
  sessionAffinity: None
  type: NodePort
status:
  loadBalancer: {}
---
apiVersion: extensions/v1
kind: Deployment
metadata:
  name: test
spec:
  revisionHistoryLimit: 2
  strategy:
    type: RollingUpdate
  replicas: 1
  selector:
    matchLabels:
      test-app: test
  template:
    metadata:
      labels:
        test-app: test
    spec:
      volumes:
        - name: config-volume
          configMap:
            name: test-config
      containers:
        - name: configmap-reload
          image: "jimmidyson/configmap-reload:v0.1"
          imagePullPolicy: "IfNotPresent"
          args:
            - --volume-dir=/etc/config
            - --webhook-url=http://localhost:3000/-/reload
          volumeMounts:
            - name: config-volume
              mountPath: /etc/config
              readOnly: true
        - name: test
          image: reyhan/docker-try-config:1.0.3
          command: ["npm", "start"]
          imagePullPolicy: Never
          livenessProbe:
            httpGet:
              path: /info
              port: 3000
            initialDelaySeconds: 90
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /info
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
          ports:
            - containerPort: 3000
          volumeMounts:
            - name: config-volume
              mountPath: /etc/config
          resources:
            requests:
              memory: "256Mi"
              cpu: "64m"
            limits:
              memory: "512Mi"
              cpu: "256m"
          env:
            - name: ENV_SQL_CLIENT
              value: "mysql"
            - name: ENV_SQL_HOST
              value: "13.67.44.182"
            - name: ENV_IS_MAINTENANCE
              valueFrom:
                configMapKeyRef:
                  name: test-config
                  key: ENV_IS_MAINTENANCE
            - name: ENV_IS_BLOWN_UP
              valueFrom:
                configMapKeyRef:
                  name: test-config
                  key: ENV_IS_BLOWN_UP

Mari kita perhatikan konfigurasi containers di atas

Jika kita perhatikan lagi, kita menambahkan kontainer jimmidyson/configmap-reload dengan beberapa konfigurasinya ke dalam Pod untuk membaca perubahan ConfigMap.

app.js
  • js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// @ts-check

const express = require("express");
const app = express();
const fs = require("fs");
const bodyParser = require('body-parser');

const mountPath = "/etc/config";
const configMapFile = "..data";

function readConfig() {
  const dir = fs.readdirSync(`${mountPath}/${configMapFile}`);

  console.log(`Available envar: ${dir}`);

  dir.forEach(env => {
    process.env[env] = fs.readFileSync(
      `${mountPath}/${configMapFile}/${env}`
    );
  });
}

app.get("/info", (req, res) => {
  res.sendStatus(200);
});

app.use(bodyParser.json());

// configmap reload webhook
app.post("/-/reload", (req, res) => {
  // read new ConfigMap value
  readConfig();

  console.log(`process.env AFTER: ${JSON.stringify(process.env)}`);

  res.sendStatus(200);
});

app.get("/isMaintenance", (req, res) => {
  const isMaintenace = process.env.ENV_IS_MAINTENANCE;

  res.status(200).send(`Is it maintenance? ${isMaintenace}`);
})

app.listen(3000, err => {
  if (err) {
    console.error(err);
    process.exit(1);
  }

  console.log(`Server is up on port 3000`);
});

Untuk kode server kita menambahkan satu endpoint supaya bisa menerima webhook dari kontainer “configmap-reload”.

Jika kita menggunakan metode seperti ini, maka alur kerjanya akan menjadi seperti berikut

  1. Kontainer configmap-reload akan melihat apakah ConfigMap yang dipasang sebagai volume mengalami perubahan atau tidak
  2. Jika ConfigMap mengalami perubahan, kontainer configmap-reload akan memberikan notifikasi kepada aplikasi yang memiliki endpoint /-/reload (sesuai dengan konfigurasi parameter --webhook-url)
  3. Aplikasi yang menerima notifikasi akan membaca ulang semua variabel lingkungan yang ada di ConfigMap lalu menyimpan ulang ke sebuah variabel global. Karena contoh di atas menggunakan NodeJS, maka akan disimpan ke variabel process.env

Dengan metode seperti ini, kita bisa implementasi “live-update” dengan bahasa pemrograman apapun. Selamat bereksperimen!


Referensi