Go Service Configuration: Step by Step instructions to configure a Go Service to read from a ConfigMap and refresh automatically when it changes. This article is similar to an earlier post on Spring Boot Series.
This article uses the Kubernetes Go Client to call respective apis that reads the configuration from the K8s ConfigMap, and then create a watcher and subscribe for any updates to the ConfigMap.
Pre-Requisites
Follow the steps as listed in this post to create a Go Service that runs on K8s and is installed using Helm. Here is how your project folder structure should look like once done:
1. Create Roles
Create appropriate roles and bind it the pod service account. This will be provide necessary permissions to the application for reading various k8s objects such as ConfigMap. Navigate to helm chart directory ms-template/templates create roles.yaml
and rolebinding.yaml
with the following content
role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "ms-template.name" . }}
labels:
{{- include "ms-template.labels" . | nindent 4 }}
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["pods","configmaps","endpoints","services"]
verbs: ["get", "watch", "list"]
rolebinding.yaml
: To attach the role created above with the pod service account.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "ms-template.name" . }}
labels:
{{- include "ms-template.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "ms-template.name" . }}
subjects:
- kind: ServiceAccount
name: {{ include "ms-template.name" . }}
2. Install Kubernetes Go Client
Run the following command in the root directory of the project.
go get k8s.io/client-go@latest
3. Create ConfigMap Instance
We will use these to store instances of configmap and k8s clientset
var (
configmap *v12.ConfigMap
configMapName = "ms-template"
clientset *kubernetes.Clientset
)
Create an instance of confirmap when your service starts
func main() {
initialize()
r := gin.Default()
r.GET("/health", healthCheck)
r.GET("/configmap", printConfigmap)
r.Run("0.0.0.0:8080")
log.SetOutput(os.Stdout)
}
func initialize() {
// initialize client
config, _ := rest.InClusterConfig()
var configErr error
clientset, configErr = kubernetes.NewForConfig(config)
if configErr != nil {
panic(configErr.Error())
}
configmaps := clientset.CoreV1().ConfigMaps("default")
initializeConfigmap(&configmaps)
}
func initializeConfigmap(configmaps *v13.ConfigMapInterface) {
var err error
cfgs := *configmaps
configmap, err = cfgs.Get(context.TODO(), configMapName, v1.GetOptions{})
if err != nil {
fmt.Printf("Unable to retreive %v, errror:%v", configMapName, err)
panic(err.Error())
}
fmt.Printf("\nCreated/Refreshed Configmap Object Value from k8s configmap %v", configMapName)
}
This will create a global configmap instance that can be used by the application. Next up lets complete the Go Service configuration by subscribing to change events from a ConfigMap.
4. Create Watcher and Subscribe
Add the following method in main.go file
func startConfigMapWatch(configmaps *v13.ConfigMapInterface) {
cfgs := *configmaps
watcher, err := cfgs.Watch(context.TODO(), v1.ListOptions{})
if err != nil {
fmt.Printf("Unable to Create a watcher on configmap %v, with errror:%v", configMapName, err)
panic(err.Error())
}
for event := range watcher.ResultChan() {
cfg := event.Object.(*v12.ConfigMap)
switch event.Type {
case watch.Modified:
if strings.Contains(cfg.ObjectMeta.Name, configMapName) {
fmt.Printf("\nConfigmap %s/%s modified", cfg.ObjectMeta.Namespace, cfg.ObjectMeta.Name)
initializeConfigmap(configmaps)
}
}
}
}
The above code creates a watcher and subscribes to any change events the configmap. On receiving a change event it will replace the existing copy of the configmaps var with the latest one. Then finally in the initializeconfigmap method add the following lines to call the above method in a separate go subroutine go startConfigMapWatch(&configmaps)
5. Create Test Endpoint for Go Service Configuration
Create a test api to print the ConfigMap object values , this will help us verify whether our go service is able to read from the ConfigMap. Add the following to the main.go.
func main() {
.....
r.GET("/configmap", printConfigmap)
.....
}
func printConfigmap(c *gin.Context) {
c.JSON(http.StatusOK, configmap.Data)
}
The code above will create an endpoint /configmap that will print the values within the data block of the configmap.
Here is how main.go file should look like once done.
6. Build Image and Deploy
Build the image with the code above and add the tag to the helm chart chart.yaml file
docker build -t ms-template:0.2 . && helm install ms-template ms-template/
and then install via helm charts, ensure that chart.yaml has the above 0.2 tag before doing so
helm install ms-template ms-template/
7. Testing Go Service Configuration
Lets test the changes, first up lets verify whether our code was able to read the Configmap
$ curl http://localhost:8080/configmap
{"app.properties":"scale=1\nchances=2\n"}
$ kubectl edit configmap ms-template
configmap/ms-template edited
$ curl http://localhost:8080/configmap
{"app.properties":"scale=1001\nchances=2\n"}
In the logs you will see the following, which verifies the modified event was received by our go program
[GIN] 2022/12/27 - 18:00:13 | 200 | 353.96µs | 192.168.65.3 | GET "/configmap"
Configmap default/ms-template modified
Created/Refreshed Configmap Object Value from k8s configmap ms-template[GIN] 2022/12/27 - 18:00:50 | 200 | 54.026µs | 10.1.0.1 | GET "/health"
[GIN] 2022/12/27 - 18:00:57 | 200 | 36.642µs | 192.168.65.3 | GET "/configmap"
Troubleshooting
Troubleshooting any failure or issues is similar to how you would do it for other Kubernetes workloads. Here are some tips on easy starting over
- If something goes wrong in helm and you want to start over just delete the chart by running
helm delete
ms-template - YAML is very sensitive to indentation, so please ensure that the indentation is proper.
- if all else fails you can still download the code from the GitHub repo and try comparing it with your setup
The entire source code for this post is available here.