• United States+1
  • United Kingdom+44
  • Afghanistan (‫افغانستان‬‎)+93
  • Albania (Shqipëri)+355
  • Algeria (‫الجزائر‬‎)+213
  • American Samoa+1684
  • Andorra+376
  • Angola+244
  • Anguilla+1264
  • Antigua and Barbuda+1268
  • Argentina+54
  • Armenia (Հայաստան)+374
  • Aruba+297
  • Australia+61
  • Austria (Österreich)+43
  • Azerbaijan (Azərbaycan)+994
  • Bahamas+1242
  • Bahrain (‫البحرين‬‎)+973
  • Bangladesh (বাংলাদেশ)+880
  • Barbados+1246
  • Belarus (Беларусь)+375
  • Belgium (België)+32
  • Belize+501
  • Benin (Bénin)+229
  • Bermuda+1441
  • Bhutan (འབྲུག)+975
  • Bolivia+591
  • Bosnia and Herzegovina (Босна и Херцеговина)+387
  • Botswana+267
  • Brazil (Brasil)+55
  • British Indian Ocean Territory+246
  • British Virgin Islands+1284
  • Brunei+673
  • Bulgaria (България)+359
  • Burkina Faso+226
  • Burundi (Uburundi)+257
  • Cambodia (កម្ពុជា)+855
  • Cameroon (Cameroun)+237
  • Canada+1
  • Cape Verde (Kabu Verdi)+238
  • Caribbean Netherlands+599
  • Cayman Islands+1345
  • Central African Republic (République centrafricaine)+236
  • Chad (Tchad)+235
  • Chile+56
  • China (中国)+86
  • Christmas Island+61
  • Cocos (Keeling) Islands+61
  • Colombia+57
  • Comoros (‫جزر القمر‬‎)+269
  • Congo (DRC) (Jamhuri ya Kidemokrasia ya Kongo)+243
  • Congo (Republic) (Congo-Brazzaville)+242
  • Cook Islands+682
  • Costa Rica+506
  • Côte d’Ivoire+225
  • Croatia (Hrvatska)+385
  • Cuba+53
  • Curaçao+599
  • Cyprus (Κύπρος)+357
  • Czech Republic (Česká republika)+420
  • Denmark (Danmark)+45
  • Djibouti+253
  • Dominica+1767
  • Dominican Republic (República Dominicana)+1
  • Ecuador+593
  • Egypt (‫مصر‬‎)+20
  • El Salvador+503
  • Equatorial Guinea (Guinea Ecuatorial)+240
  • Eritrea+291
  • Estonia (Eesti)+372
  • Ethiopia+251
  • Falkland Islands (Islas Malvinas)+500
  • Faroe Islands (Føroyar)+298
  • Fiji+679
  • Finland (Suomi)+358
  • France+33
  • French Guiana (Guyane française)+594
  • French Polynesia (Polynésie française)+689
  • Gabon+241
  • Gambia+220
  • Georgia (საქართველო)+995
  • Germany (Deutschland)+49
  • Ghana (Gaana)+233
  • Gibraltar+350
  • Greece (Ελλάδα)+30
  • Greenland (Kalaallit Nunaat)+299
  • Grenada+1473
  • Guadeloupe+590
  • Guam+1671
  • Guatemala+502
  • Guernsey+44
  • Guinea (Guinée)+224
  • Guinea-Bissau (Guiné Bissau)+245
  • Guyana+592
  • Haiti+509
  • Honduras+504
  • Hong Kong (香港)+852
  • Hungary (Magyarország)+36
  • Iceland (Ísland)+354
  • India (भारत)+91
  • Indonesia+62
  • Iran (‫ایران‬‎)+98
  • Iraq (‫العراق‬‎)+964
  • Ireland+353
  • Isle of Man+44
  • Israel (‫ישראל‬‎)+972
  • Italy (Italia)+39
  • Jamaica+1876
  • Japan (日本)+81
  • Jersey+44
  • Jordan (‫الأردن‬‎)+962
  • Kazakhstan (Казахстан)+7
  • Kenya+254
  • Kiribati+686
  • Kosovo+383
  • Kuwait (‫الكويت‬‎)+965
  • Kyrgyzstan (Кыргызстан)+996
  • Laos (ລາວ)+856
  • Latvia (Latvija)+371
  • Lebanon (‫لبنان‬‎)+961
  • Lesotho+266
  • Liberia+231
  • Libya (‫ليبيا‬‎)+218
  • Liechtenstein+423
  • Lithuania (Lietuva)+370
  • Luxembourg+352
  • Macau (澳門)+853
  • Macedonia (FYROM) (Македонија)+389
  • Madagascar (Madagasikara)+261
  • Malawi+265
  • Malaysia+60
  • Maldives+960
  • Mali+223
  • Malta+356
  • Marshall Islands+692
  • Martinique+596
  • Mauritania (‫موريتانيا‬‎)+222
  • Mauritius (Moris)+230
  • Mayotte+262
  • Mexico (México)+52
  • Micronesia+691
  • Moldova (Republica Moldova)+373
  • Monaco+377
  • Mongolia (Монгол)+976
  • Montenegro (Crna Gora)+382
  • Montserrat+1664
  • Morocco (‫المغرب‬‎)+212
  • Mozambique (Moçambique)+258
  • Myanmar (Burma) (မြန်မာ)+95
  • Namibia (Namibië)+264
  • Nauru+674
  • Nepal (नेपाल)+977
  • Netherlands (Nederland)+31
  • New Caledonia (Nouvelle-Calédonie)+687
  • New Zealand+64
  • Nicaragua+505
  • Niger (Nijar)+227
  • Nigeria+234
  • Niue+683
  • Norfolk Island+672
  • North Korea (조선 민주주의 인민 공화국)+850
  • Northern Mariana Islands+1670
  • Norway (Norge)+47
  • Oman (‫عُمان‬‎)+968
  • Pakistan (‫پاکستان‬‎)+92
  • Palau+680
  • Palestine (‫فلسطين‬‎)+970
  • Panama (Panamá)+507
  • Papua New Guinea+675
  • Paraguay+595
  • Peru (Perú)+51
  • Philippines+63
  • Poland (Polska)+48
  • Portugal+351
  • Puerto Rico+1
  • Qatar (‫قطر‬‎)+974
  • Réunion (La Réunion)+262
  • Romania (România)+40
  • Russia (Россия)+7
  • Rwanda+250
  • Saint Barthélemy (Saint-Barthélemy)+590
  • Saint Helena+290
  • Saint Kitts and Nevis+1869
  • Saint Lucia+1758
  • Saint Martin (Saint-Martin (partie française))+590
  • Saint Pierre and Miquelon (Saint-Pierre-et-Miquelon)+508
  • Saint Vincent and the Grenadines+1784
  • Samoa+685
  • San Marino+378
  • São Tomé and Príncipe (São Tomé e Príncipe)+239
  • Saudi Arabia (‫المملكة العربية السعودية‬‎)+966
  • Senegal (Sénégal)+221
  • Serbia (Србија)+381
  • Seychelles+248
  • Sierra Leone+232
  • Singapore+65
  • Sint Maarten+1721
  • Slovakia (Slovensko)+421
  • Slovenia (Slovenija)+386
  • Solomon Islands+677
  • Somalia (Soomaaliya)+252
  • South Africa+27
  • South Korea (대한민국)+82
  • South Sudan (‫جنوب السودان‬‎)+211
  • Spain (España)+34
  • Sri Lanka (ශ්‍රී ලංකාව)+94
  • Sudan (‫السودان‬‎)+249
  • Suriname+597
  • Svalbard and Jan Mayen+47
  • Swaziland+268
  • Sweden (Sverige)+46
  • Switzerland (Schweiz)+41
  • Syria (‫سوريا‬‎)+963
  • Taiwan (台灣)+886
  • Tajikistan+992
  • Tanzania+255
  • Thailand (ไทย)+66
  • Timor-Leste+670
  • Togo+228
  • Tokelau+690
  • Tonga+676
  • Trinidad and Tobago+1868
  • Tunisia (‫تونس‬‎)+216
  • Turkey (Türkiye)+90
  • Turkmenistan+993
  • Turks and Caicos Islands+1649
  • Tuvalu+688
  • U.S. Virgin Islands+1340
  • Uganda+256
  • Ukraine (Україна)+380
  • United Arab Emirates (‫الإمارات العربية المتحدة‬‎)+971
  • United Kingdom+44
  • United States+1
  • Uruguay+598
  • Uzbekistan (Oʻzbekiston)+998
  • Vanuatu+678
  • Vatican City (Città del Vaticano)+39
  • Venezuela+58
  • Vietnam (Việt Nam)+84
  • Wallis and Futuna+681
  • Western Sahara (‫الصحراء الغربية‬‎)+212
  • Yemen (‫اليمن‬‎)+967
  • Zambia+260
  • Zimbabwe+263
  • Åland Islands+358
Thanks! We'll be in touch in the next 12 hours
Oops! Something went wrong while submitting the form.

Getting Started With Kubernetes Operators (Golang Based) - Part 3

Akash Gautam

Cloud & DevOps

Introduction

In the first, getting started with Kubernetes operators (Helm based), and the second part, getting started with Kubernetes operators (Ansible based), of this Introduction to Kubernetes operators blog series we learned various concepts related to Kubernetes operators and created a Helm based operator and an Ansible based operator respectively. In this final part, we will build a Golang based operator. In case of Helm based operators, we were executing a helm chart when changes were made to the custom object type of our application, similarly in the case of an Ansible based operator we executed an Ansible role. In case of Golang based operator we write the code for the action we need to perform (reconcile logic) whenever the state of our custom object change, this makes the Golang based operators quite powerful and flexible, at the same time making them the most complex to build out of the 3 types.

What Will We Build?

The database server we deployed as part of our book store app in previous blogs didn’t have any persistent volume attached to it and we would lose data in case the pod restarts, to avoid this we will attach a persistent volume attached to the host (K8s worker nodes ) and run our database as an statefulset rather than a deployment. We will also add a feature to expand the persistent volume associated with the mongodb pod.

Building the Operator

1. Set up the project:  

operator-sdk new bookstore-operator --dep-manager=dep

INFO[0000] Generating api version blog.velotio.com/v1alpha1 for kind BookStore.
INFO[0000] Created pkg/apis/blog/group.go
INFO[0001] Created pkg/apis/blog/v1alpha1/bookstore_types.go
INFO[0001] Created pkg/apis/addtoscheme_blog_v1alpha1.go
INFO[0001] Created pkg/apis/blog/v1alpha1/register.go
INFO[0001] Created pkg/apis/blog/v1alpha1/doc.go
INFO[0001] Created deploy/crds/blog.velotio.com_v1alpha1_bookstore_cr.yaml
INFO[0009] Created deploy/crds/blog.velotio.com_bookstores_crd.yaml
INFO[0009] Running deepcopy code-generation for Custom Resource group versions: [blog:[v1alpha1], ]
INFO[0010] Code-generation complete.
INFO[0010] Running OpenAPI code-generation for Custom Resource group versions: [blog:[v1alpha1], ]
INFO[0011] Created deploy/crds/blog.velotio.com_bookstores_crd.yaml
INFO[0011] Code-generation complete.
INFO[0011] API generation complete.

The above command creates the bookstore-operator folder in our $GOPATH/src, here we have set the --dep-manager as dep which signifies we want to use dep for managing dependencies, by default it uses go modules for managing dependencies. Similar to what we have seen earlier the operator sdk creates all the necessary folder structure for us inside the bookstore-operator folder.

2. Add the custom resource definition

operator-sdk add api --api-version=blog.velotio.com/v1alpha1 --kind=BookStore

The above command creates the CRD and CR for the BookStore type. It also creates the golang structs (pkg/apis/blog/v1alpha1/bookstore_types.go)  for BookStore types.  It also registers the custom type (pkg/apis/blog/v1alpha1/register.go) with schema and generates deep-copy methods as well. Here we can see that all the generic tasks are being done by the operator framework itself allowing us to focus on building and object and the controller. We will update the spec of our BookStore object later. We will update the spec of BookStore type to include two custom types BookApp and BookDB.

type BookStoreSpec struct {
BookApp BookApp `json:"bookApp,omitempty"`
BookDB BookDB `json:"bookDB,omitempty"`
}
type BookApp struct {
Repository string `json:"repository,omitempty"`
Tag string `json:"tag,omitempty"`
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
Replicas int32 `json:"replicas,omitempty"`
Port int32 `json:"port,omitempty"`
TargetPort int `json:"targetPort,omitempty"`
ServiceType corev1.ServiceType `json:"serviceType,omitempty"`
}
type BookDB struct {
Repository string `json:"repository,omitempty"`
Tag string `json:"tag,omitempty"`
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
Replicas int32 `json:"replicas,omitempty"`
Port int32 `json:"port,omitempty"`
DBSize resource.Quantity `json:"dbSize,omitempty"`
}

Let’s also update the BookStore CR (blog.velotio.com_v1alpha1_bookstore_cr.yaml)

apiVersion: blog.velotio.com/v1alpha1
kind: BookStore
metadata:name: example-bookstore
spec:
bookApp:
repository: "akash125/pyapp"
tag: latest
imagePullPolicy: "IfNotPresent"
replicas: 1
port: 80
targetPort: 3000
serviceType: "LoadBalancer"
bookDB:
repository: "mongo"
tag: latest
imagePullPolicy: "IfNotPresent"
replicas: 1
port: 27017
dbSize: 2Gi

3. Add the bookstore controller

operator-sdk add controller --api-version=blog.velotio.com/v1alpha1 --kind=BookStore

INFO[0000] Generating controller version blog.velotio.com/v1alpha1 for kind BookStore.
INFO[0000] Created pkg/controller/bookstore/bookstore_controller.go
INFO[0000] Created pkg/controller/add_bookstore.go
INFO[0000] Controller generation complete.

The above command adds the bookstore controller (pkg/controller/bookstore/bookstore_controller.go) to the project and also adds it to the manager.

If we take a look at the add function in the bookstore_controller.go file we can see that a new controller is created here and added to the manager so that the manager can start the controller when it (manager) comes up,  the add(mgr manager.Manager, r reconcile.Reconciler) is called by the public function Add(mgr manager.Manager) which also creates a new reconciler objects and passes it to the add where the controller is associated with the reconciler, in the add function we also set the type of object (BookStore) which the controller will watch.

// Watch for changes to primary resource BookStore
err = c.Watch(&source.Kind{Type: &blogv1alpha1.BookStore{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}
view raw watch.go hosted with ❤ by GitHub

This ensures that for any events related to any object of BookStore type, a reconcile request (a namespace/name key) is sent to the Reconcile method associated with the reconciler object (ReconcileBookStore) here.

4. Build the reconcile logic

The reconcile logic is implemented inside the Reconcile method of the reconciler object of the custom type which implements the reconcile loop.

As a part of our reconcile logic we will do the following

  1. Create the bookstore app deployment if it doesn’t exist.
  2. Create the bookstore app service if it doesn’t exist.
  3. Create the Mongodb statefulset if it doesn’t exist.
  4. Create the Mongodb service if it doesn’t exist.
  5. Ensure deployments and services match their desired configurations like the replica count, image tag, service port, size of the PV associated with the Mongodb statefulset etc.

 There are three possible events that can happen with the BookStore object

  1. The object got created: Whenever an object of kind BookStore is created we create all the k8s resources we mentioned above
  2. The object has been updated: When the object gets updated then we update all the k8s resources associated with it..
  3. The object has been deleted: When the object gets deleted we don’t need to do anything as while creating the K8s objects we will set the `BookStore` type as its owner which will ensure that all the K8s objects associated with it gets automatically deleted when we delete the object.

On receiving the reconcile request the first step if to lookup for the object.

func (r *ReconcileBookStore) Reconcile(request reconcile.Request) (reconcile.Result, error) {
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.Info("Reconciling BookStore")
// Fetch the BookStore instance
bookstore := &blogv1alpha1.BookStore{}
err := r.client.Get(context.TODO(), request.NamespacedName, bookstore)
view raw lookup.go hosted with ❤ by GitHub

If the object is not found, we assume that it got deleted and don’t requeue the request considering the reconcile to be successful.

If any error occurs while doing the reconcile then we return the error and whenever we return non nil error value then controller requeues the request.

In the reconcile logic we call the BookStore method which creates or updates all the k8s objects associated with the BookStore objects based on whether the object has been created or updated.

func (r *ReconcileBookStore) BookStore(bookstore *blogv1alpha1.BookStore) error {
reqLogger := log.WithValues("Namespace", bookstore.Namespace)
mongoDBSvc := getmongoDBSvc(bookstore)
msvc := &corev1.Service{}
err := r.client.Get(context.TODO(), types.NamespacedName{Name: "mongodb-service", Namespace: bookstore.Namespace}, msvc)
if err != nil {
if errors.IsNotFound(err) {
controllerutil.SetControllerReference(bookstore, mongoDBSvc, r.scheme)
err = r.client.Create(context.TODO(), mongoDBSvc)
if err != nil { return err }
} else { return err }
} else if !reflect.DeepEqual(mongoDBSvc.Spec, msvc.Spec) {
mongoDBSvc.ObjectMeta = msvc.ObjectMeta
controllerutil.SetControllerReference(bookstore, mongoDBSvc, r.scheme)
err = r.client.Update(context.TODO(), mongoDBSvc)
if err != nil { return err }
reqLogger.Info("mongodb-service updated")
}
mongoDBSS := getMongoDBStatefulsets(bookstore)
mss := &appsv1.StatefulSet{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: "mongodb", Namespace: bookstore.Namespace}, mss)
if err != nil {
if errors.IsNotFound(err) {
reqLogger.Info("mongodb statefulset not found, will be created")
controllerutil.SetControllerReference(bookstore, mongoDBSS, r.scheme)
err = r.client.Create(context.TODO(), mongoDBSS)
if err != nil { return err }
} else {
reqLogger.Info("failed to get mongodb statefulset")
return err
}
} else if !reflect.DeepEqual(mongoDBSS.Spec, mss.Spec) {
r.UpdateVolume(bookstore)
mongoDBSS.ObjectMeta = mss.ObjectMeta
mongoDBSS.Spec.VolumeClaimTemplates = mss.Spec.VolumeClaimTemplates
controllerutil.SetControllerReference(bookstore, mongoDBSS, r.scheme)
err = r.client.Update(context.TODO(), mongoDBSS)
if err != nil { return err }
reqLogger.Info("mongodb statefulset updated")
}
bookStoreSvc := getBookStoreAppSvc(bookstore)
bsvc := &corev1.Service{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: "bookstore-svc", Namespace: bookstore.Namespace}, bsvc)
if err != nil {
if errors.IsNotFound(err) {
controllerutil.SetControllerReference(bookstore, bookStoreSvc, r.scheme)
err = r.client.Create(context.TODO(), bookStoreSvc)
if err != nil { return err }
} else {
reqLogger.Info("failed to get bookstore service")
return err
}
} else if !reflect.DeepEqual(bookStoreSvc.Spec, bsvc.Spec) {
bookStoreSvc.ObjectMeta = bsvc.ObjectMeta
bookStoreSvc.Spec.ClusterIP = bsvc.Spec.ClusterIP
controllerutil.SetControllerReference(bookstore, bookStoreSvc, r.scheme)
err = r.client.Update(context.TODO(), bookStoreSvc)
if err != nil { return err }
reqLogger.Info("bookstore service updated")
}
bookStoreDep := getBookStoreDeploy(bookstore)
bsdep := &appsv1.Deployment{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: "bookstore", Namespace: bookstore.Namespace}, bsdep)
if err != nil {
if errors.IsNotFound(err) {
controllerutil.SetControllerReference(bookstore, bookStoreDep, r.scheme)
err = r.client.Create(context.TODO(), bookStoreDep)
if err != nil { return err }
} else {
reqLogger.Info("failed to get bookstore deployment")
return err
}
} else if !reflect.DeepEqual(bookStoreDep.Spec, bsdep.Spec) {
bookStoreDep.ObjectMeta = bsdep.ObjectMeta
controllerutil.SetControllerReference(bookstore, bookStoreDep, r.scheme)
err = r.client.Update(context.TODO(), bookStoreDep)
if err != nil { return err }
reqLogger.Info("bookstore deployment updated")
}
r.client.Status().Update(context.TODO(), bookstore)
return nil
}
view raw BookStore.go hosted with ❤ by GitHub

The implementation of the above method is a bit hacky but gives an idea of the flow. In the above function, we can see that we are setting the BookStore type as an owner for all the resources controllerutil.SetControllerReference(c, bookStoreDep, r.scheme) as we had discussed earlier. If we look at the owner reference for these objects we would see something like this.

ownerReferences:
- apiVersion: blog.velotio.com/v1alpha1
blockOwnerDeletion: true
controller: true
kind: BookStore
name: example-bookstore
uid: 0ef42889-deb4-11e9-ba56-42010a800256
resourceVersion: "20295281"
view raw ownerref.yaml hosted with ❤ by GitHub

5.  Deploy the operator and verify its working

The approach to deploy and verify the working of the bookstore application is similar to what we did in the previous two blogs the only difference being that now we have deployed the Mongodb as a stateful set and even if we restart the pod we will see that the information that we stored will still be available.

Kubernetes Golang 1.png

6. Verify volume expansion

For updating the volume associated with the mongodb instance we first need to update the size of the volume we specified while creating the bookstore object. In the example above I had set it to 2GB let’s update it to 3GB and update the bookstore object.

Once the bookstore object is updated if we describe the mongodb PVC we will see that it still has 2GB PV but the conditions we will see something like this.

Conditions:
Type Status LastProbeTime LastTransitionTime Reason Message
---- ------ ----------------- ------------------ ------ -------
FileSystemResizePending True Mon, 01 Jan 0001 00:00:00 +0000 Mon, 30 Sep 2019 15:07:01 +0530 Waiting for user to (re-)start a pod to finish file system resize of volume on node.
@velotiotech

It is clear from the message that we need to restart the pod for resizing of volume to reflect. Once we delete the pod it will get restarted and the PVC will get updated to reflect the expanded volume size.

Kubernetes Golang 2.png

The complete code is available here.

Conclusion

Golang based operators are built mostly for stateful applications like databases. The operator can automate complex operational tasks allow us to run applications with ease. At the same time, building and maintaining it can be quite complex and we should build one only when we are fully convinced that our requirements can’t be met with any other type of operator. Operators are an interesting and emerging area in Kubernetes and I hope this blog series on getting started with it help the readers in learning the basics of it.

Get the latest engineering blogs delivered straight to your inbox.
No spam. Only expert insights.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings

Getting Started With Kubernetes Operators (Golang Based) - Part 3

Introduction

In the first, getting started with Kubernetes operators (Helm based), and the second part, getting started with Kubernetes operators (Ansible based), of this Introduction to Kubernetes operators blog series we learned various concepts related to Kubernetes operators and created a Helm based operator and an Ansible based operator respectively. In this final part, we will build a Golang based operator. In case of Helm based operators, we were executing a helm chart when changes were made to the custom object type of our application, similarly in the case of an Ansible based operator we executed an Ansible role. In case of Golang based operator we write the code for the action we need to perform (reconcile logic) whenever the state of our custom object change, this makes the Golang based operators quite powerful and flexible, at the same time making them the most complex to build out of the 3 types.

What Will We Build?

The database server we deployed as part of our book store app in previous blogs didn’t have any persistent volume attached to it and we would lose data in case the pod restarts, to avoid this we will attach a persistent volume attached to the host (K8s worker nodes ) and run our database as an statefulset rather than a deployment. We will also add a feature to expand the persistent volume associated with the mongodb pod.

Building the Operator

1. Set up the project:  

operator-sdk new bookstore-operator --dep-manager=dep

INFO[0000] Generating api version blog.velotio.com/v1alpha1 for kind BookStore.
INFO[0000] Created pkg/apis/blog/group.go
INFO[0001] Created pkg/apis/blog/v1alpha1/bookstore_types.go
INFO[0001] Created pkg/apis/addtoscheme_blog_v1alpha1.go
INFO[0001] Created pkg/apis/blog/v1alpha1/register.go
INFO[0001] Created pkg/apis/blog/v1alpha1/doc.go
INFO[0001] Created deploy/crds/blog.velotio.com_v1alpha1_bookstore_cr.yaml
INFO[0009] Created deploy/crds/blog.velotio.com_bookstores_crd.yaml
INFO[0009] Running deepcopy code-generation for Custom Resource group versions: [blog:[v1alpha1], ]
INFO[0010] Code-generation complete.
INFO[0010] Running OpenAPI code-generation for Custom Resource group versions: [blog:[v1alpha1], ]
INFO[0011] Created deploy/crds/blog.velotio.com_bookstores_crd.yaml
INFO[0011] Code-generation complete.
INFO[0011] API generation complete.

The above command creates the bookstore-operator folder in our $GOPATH/src, here we have set the --dep-manager as dep which signifies we want to use dep for managing dependencies, by default it uses go modules for managing dependencies. Similar to what we have seen earlier the operator sdk creates all the necessary folder structure for us inside the bookstore-operator folder.

2. Add the custom resource definition

operator-sdk add api --api-version=blog.velotio.com/v1alpha1 --kind=BookStore

The above command creates the CRD and CR for the BookStore type. It also creates the golang structs (pkg/apis/blog/v1alpha1/bookstore_types.go)  for BookStore types.  It also registers the custom type (pkg/apis/blog/v1alpha1/register.go) with schema and generates deep-copy methods as well. Here we can see that all the generic tasks are being done by the operator framework itself allowing us to focus on building and object and the controller. We will update the spec of our BookStore object later. We will update the spec of BookStore type to include two custom types BookApp and BookDB.

type BookStoreSpec struct {
BookApp BookApp `json:"bookApp,omitempty"`
BookDB BookDB `json:"bookDB,omitempty"`
}
type BookApp struct {
Repository string `json:"repository,omitempty"`
Tag string `json:"tag,omitempty"`
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
Replicas int32 `json:"replicas,omitempty"`
Port int32 `json:"port,omitempty"`
TargetPort int `json:"targetPort,omitempty"`
ServiceType corev1.ServiceType `json:"serviceType,omitempty"`
}
type BookDB struct {
Repository string `json:"repository,omitempty"`
Tag string `json:"tag,omitempty"`
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
Replicas int32 `json:"replicas,omitempty"`
Port int32 `json:"port,omitempty"`
DBSize resource.Quantity `json:"dbSize,omitempty"`
}

Let’s also update the BookStore CR (blog.velotio.com_v1alpha1_bookstore_cr.yaml)

apiVersion: blog.velotio.com/v1alpha1
kind: BookStore
metadata:name: example-bookstore
spec:
bookApp:
repository: "akash125/pyapp"
tag: latest
imagePullPolicy: "IfNotPresent"
replicas: 1
port: 80
targetPort: 3000
serviceType: "LoadBalancer"
bookDB:
repository: "mongo"
tag: latest
imagePullPolicy: "IfNotPresent"
replicas: 1
port: 27017
dbSize: 2Gi

3. Add the bookstore controller

operator-sdk add controller --api-version=blog.velotio.com/v1alpha1 --kind=BookStore

INFO[0000] Generating controller version blog.velotio.com/v1alpha1 for kind BookStore.
INFO[0000] Created pkg/controller/bookstore/bookstore_controller.go
INFO[0000] Created pkg/controller/add_bookstore.go
INFO[0000] Controller generation complete.

The above command adds the bookstore controller (pkg/controller/bookstore/bookstore_controller.go) to the project and also adds it to the manager.

If we take a look at the add function in the bookstore_controller.go file we can see that a new controller is created here and added to the manager so that the manager can start the controller when it (manager) comes up,  the add(mgr manager.Manager, r reconcile.Reconciler) is called by the public function Add(mgr manager.Manager) which also creates a new reconciler objects and passes it to the add where the controller is associated with the reconciler, in the add function we also set the type of object (BookStore) which the controller will watch.

// Watch for changes to primary resource BookStore
err = c.Watch(&source.Kind{Type: &blogv1alpha1.BookStore{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}
view raw watch.go hosted with ❤ by GitHub

This ensures that for any events related to any object of BookStore type, a reconcile request (a namespace/name key) is sent to the Reconcile method associated with the reconciler object (ReconcileBookStore) here.

4. Build the reconcile logic

The reconcile logic is implemented inside the Reconcile method of the reconciler object of the custom type which implements the reconcile loop.

As a part of our reconcile logic we will do the following

  1. Create the bookstore app deployment if it doesn’t exist.
  2. Create the bookstore app service if it doesn’t exist.
  3. Create the Mongodb statefulset if it doesn’t exist.
  4. Create the Mongodb service if it doesn’t exist.
  5. Ensure deployments and services match their desired configurations like the replica count, image tag, service port, size of the PV associated with the Mongodb statefulset etc.

 There are three possible events that can happen with the BookStore object

  1. The object got created: Whenever an object of kind BookStore is created we create all the k8s resources we mentioned above
  2. The object has been updated: When the object gets updated then we update all the k8s resources associated with it..
  3. The object has been deleted: When the object gets deleted we don’t need to do anything as while creating the K8s objects we will set the `BookStore` type as its owner which will ensure that all the K8s objects associated with it gets automatically deleted when we delete the object.

On receiving the reconcile request the first step if to lookup for the object.

func (r *ReconcileBookStore) Reconcile(request reconcile.Request) (reconcile.Result, error) {
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.Info("Reconciling BookStore")
// Fetch the BookStore instance
bookstore := &blogv1alpha1.BookStore{}
err := r.client.Get(context.TODO(), request.NamespacedName, bookstore)
view raw lookup.go hosted with ❤ by GitHub

If the object is not found, we assume that it got deleted and don’t requeue the request considering the reconcile to be successful.

If any error occurs while doing the reconcile then we return the error and whenever we return non nil error value then controller requeues the request.

In the reconcile logic we call the BookStore method which creates or updates all the k8s objects associated with the BookStore objects based on whether the object has been created or updated.

func (r *ReconcileBookStore) BookStore(bookstore *blogv1alpha1.BookStore) error {
reqLogger := log.WithValues("Namespace", bookstore.Namespace)
mongoDBSvc := getmongoDBSvc(bookstore)
msvc := &corev1.Service{}
err := r.client.Get(context.TODO(), types.NamespacedName{Name: "mongodb-service", Namespace: bookstore.Namespace}, msvc)
if err != nil {
if errors.IsNotFound(err) {
controllerutil.SetControllerReference(bookstore, mongoDBSvc, r.scheme)
err = r.client.Create(context.TODO(), mongoDBSvc)
if err != nil { return err }
} else { return err }
} else if !reflect.DeepEqual(mongoDBSvc.Spec, msvc.Spec) {
mongoDBSvc.ObjectMeta = msvc.ObjectMeta
controllerutil.SetControllerReference(bookstore, mongoDBSvc, r.scheme)
err = r.client.Update(context.TODO(), mongoDBSvc)
if err != nil { return err }
reqLogger.Info("mongodb-service updated")
}
mongoDBSS := getMongoDBStatefulsets(bookstore)
mss := &appsv1.StatefulSet{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: "mongodb", Namespace: bookstore.Namespace}, mss)
if err != nil {
if errors.IsNotFound(err) {
reqLogger.Info("mongodb statefulset not found, will be created")
controllerutil.SetControllerReference(bookstore, mongoDBSS, r.scheme)
err = r.client.Create(context.TODO(), mongoDBSS)
if err != nil { return err }
} else {
reqLogger.Info("failed to get mongodb statefulset")
return err
}
} else if !reflect.DeepEqual(mongoDBSS.Spec, mss.Spec) {
r.UpdateVolume(bookstore)
mongoDBSS.ObjectMeta = mss.ObjectMeta
mongoDBSS.Spec.VolumeClaimTemplates = mss.Spec.VolumeClaimTemplates
controllerutil.SetControllerReference(bookstore, mongoDBSS, r.scheme)
err = r.client.Update(context.TODO(), mongoDBSS)
if err != nil { return err }
reqLogger.Info("mongodb statefulset updated")
}
bookStoreSvc := getBookStoreAppSvc(bookstore)
bsvc := &corev1.Service{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: "bookstore-svc", Namespace: bookstore.Namespace}, bsvc)
if err != nil {
if errors.IsNotFound(err) {
controllerutil.SetControllerReference(bookstore, bookStoreSvc, r.scheme)
err = r.client.Create(context.TODO(), bookStoreSvc)
if err != nil { return err }
} else {
reqLogger.Info("failed to get bookstore service")
return err
}
} else if !reflect.DeepEqual(bookStoreSvc.Spec, bsvc.Spec) {
bookStoreSvc.ObjectMeta = bsvc.ObjectMeta
bookStoreSvc.Spec.ClusterIP = bsvc.Spec.ClusterIP
controllerutil.SetControllerReference(bookstore, bookStoreSvc, r.scheme)
err = r.client.Update(context.TODO(), bookStoreSvc)
if err != nil { return err }
reqLogger.Info("bookstore service updated")
}
bookStoreDep := getBookStoreDeploy(bookstore)
bsdep := &appsv1.Deployment{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: "bookstore", Namespace: bookstore.Namespace}, bsdep)
if err != nil {
if errors.IsNotFound(err) {
controllerutil.SetControllerReference(bookstore, bookStoreDep, r.scheme)
err = r.client.Create(context.TODO(), bookStoreDep)
if err != nil { return err }
} else {
reqLogger.Info("failed to get bookstore deployment")
return err
}
} else if !reflect.DeepEqual(bookStoreDep.Spec, bsdep.Spec) {
bookStoreDep.ObjectMeta = bsdep.ObjectMeta
controllerutil.SetControllerReference(bookstore, bookStoreDep, r.scheme)
err = r.client.Update(context.TODO(), bookStoreDep)
if err != nil { return err }
reqLogger.Info("bookstore deployment updated")
}
r.client.Status().Update(context.TODO(), bookstore)
return nil
}
view raw BookStore.go hosted with ❤ by GitHub

The implementation of the above method is a bit hacky but gives an idea of the flow. In the above function, we can see that we are setting the BookStore type as an owner for all the resources controllerutil.SetControllerReference(c, bookStoreDep, r.scheme) as we had discussed earlier. If we look at the owner reference for these objects we would see something like this.

ownerReferences:
- apiVersion: blog.velotio.com/v1alpha1
blockOwnerDeletion: true
controller: true
kind: BookStore
name: example-bookstore
uid: 0ef42889-deb4-11e9-ba56-42010a800256
resourceVersion: "20295281"
view raw ownerref.yaml hosted with ❤ by GitHub

5.  Deploy the operator and verify its working

The approach to deploy and verify the working of the bookstore application is similar to what we did in the previous two blogs the only difference being that now we have deployed the Mongodb as a stateful set and even if we restart the pod we will see that the information that we stored will still be available.

Kubernetes Golang 1.png

6. Verify volume expansion

For updating the volume associated with the mongodb instance we first need to update the size of the volume we specified while creating the bookstore object. In the example above I had set it to 2GB let’s update it to 3GB and update the bookstore object.

Once the bookstore object is updated if we describe the mongodb PVC we will see that it still has 2GB PV but the conditions we will see something like this.

Conditions:
Type Status LastProbeTime LastTransitionTime Reason Message
---- ------ ----------------- ------------------ ------ -------
FileSystemResizePending True Mon, 01 Jan 0001 00:00:00 +0000 Mon, 30 Sep 2019 15:07:01 +0530 Waiting for user to (re-)start a pod to finish file system resize of volume on node.
@velotiotech

It is clear from the message that we need to restart the pod for resizing of volume to reflect. Once we delete the pod it will get restarted and the PVC will get updated to reflect the expanded volume size.

Kubernetes Golang 2.png

The complete code is available here.

Conclusion

Golang based operators are built mostly for stateful applications like databases. The operator can automate complex operational tasks allow us to run applications with ease. At the same time, building and maintaining it can be quite complex and we should build one only when we are fully convinced that our requirements can’t be met with any other type of operator. Operators are an interesting and emerging area in Kubernetes and I hope this blog series on getting started with it help the readers in learning the basics of it.

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings