Let's understand OAM and Napptive

Dipankar DasDipankar Das
8 min read

Introduction

Important question: What is OAM and Napptive?

  • OAM stands for Open Application Model its a method to simplify the Kubernetes manifest to be easily understandable, and maintainable. The key component why companies have started using these is because it provides a layer of abstraction to the Kubernetes objects and provides development teams with a template for their Kubernetes manifest without the need to gain expertise on Kubernetes, also due to its simplistic approach it's far better maintainable then Kubernetes objects and the operations/platform teams can tailor-made the template to their specific or custom Kubernetes objects still being same for the development teams

    • It aims to be app-centric means the manifests are more towards the application rather than infrastructure

    • https://oam.dev/

  • Napptive is a company that provides anyone to deploy their OAM templates to their SaaS offering having a catalog so that you can pick and choose which app to deploy

    • it is one of the company among many who have started to use OAM for their products like (Kubevela, Crossplane, etc.)

How does OAM work

It provides a standard way to define and describe applications and their components, making it easier for developers and operators to work together.

It can deployed in any existing Kubernetes cluster, any resource with specific apiVersion core.oam.dev/v1alphaXY is first translated to equavalent kubernetes resources and is deployed to the kubernetes cluster, so you may think how can that be possible.

I bet you know the answer, CRD is almost similar just that using it you can create your own kubernetes resources by manipulating different kubernetes components

but for this something called as cue is used to transform the OAM template ==>> K8s manifests

OAM works by using a few key concepts:

  1. Components: These are the building blocks of an application. Components can be anything from a microservice to a database. In OAM, components are defined using a ComponentSchematic, which describes the properties and characteristics of the component. For example:
apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
  name: my-web-app
spec:
  type: webservice
  settings:
    image: example/my-web-app:1.0.0
    port: 8080
  1. ApplicationConfigurations: These are used to assemble and configure components into a complete application. ApplicationConfigurations define how components are connected, scaled, and updated. For example:
# it is used to create custom applicationConfiguration
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
metadata:
  name: my-web-app-config
spec:
  components:
    - componentName: my-web-app
      instanceName: my-web-app-instance
      parameterValues:
        - name: port
          value: 8080
  1. Traits: These are optional, reusable attributes that can be attached to components to modify their behavior or configuration. Traits can be used to specify things like auto-scaling, ingress, and resource limits. For example:
# it defines custom traits to the existing components
# this trait maps to HorizontalPodAutoscaler
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
  name: manualscalertraits.core.oam.dev
spec:
  appliesToWorkloads:
    - core.oam.dev/v1alpha2.Server
  schematic:
    cue:
      template: |
        output: {
          "apiVersion": "autoscaling/v2beta2"
          "kind":       "HorizontalPodAutoscaler"
          "metadata": {
            "name": context.name
          }
          "spec": {
            "scaleTargetRef": {
              "apiVersion": "apps/v1"
              "kind":       "Deployment"
              "name":       context.name
            }
            "minReplicas": parameter.min
            "maxReplicas": parameter.max
          }
        }
        parameter: {
          min: *1 | int
          max: *10 | int
        }

The basic flow of OAM templates

apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: hackathon-application
spec:
    components:
    - name: Component1
      type: XyZ
      ...
      properties:
          ....
      traits:
          - trait 1
           ....
          - trait n

    - name: Component2
      type: XyZ
      ....

    workflows:
        steps:
            - deployment step 1
            - deployment step 2
            ...
    policies:
        - policy 1
        - policy 2
        ....

Here

the application acts like groups of various components of a single application

components helps us to specify what kind of component we want like deployment ?, StatefulSet ? and many more

traits help us to modify each of the existing components

workflows helps us to make deployment of each component in some defined stages like first database should be deployed before the application

policies it provides the ways to modify the default properties like interval of health checks and more

Let's show how we can convert an existing Kubernetes manifest to OAM based

using the GitHub link below as a reference

through this, we will go together through the various component conversion

  1. Simple Redis instance

  2. how to solve when the Kubernetes object you want to use is not available

  3. well well I know many people will say why Go but yes we are using Go gin server as an API server with some specific route that can be used to add, get, and delete data to/from the Redis instance

Need for custom Component for configMap also for secrets

In Cue their 2 main components

parameter we define what all input user has to specify

output is the resultant kubernetes manifest in json format

its kind of a mapping

apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
  name: configmap    # it will be the type of component
spec:
  schematic:
    cue:
      template: |
        parameter: {
          // +usage=Name of the ConfigMap
          name: string
          // +usage=Key-value pairs to be stored in the ConfigMap
          data: [string]: string
          // +usage=Key-value pairs for labels
          labels: [string]: string // is a map[string]: string
        }
        output: {
          apiVersion: "v1"
          kind: "ConfigMap"
          metadata: {
            name: parameter.name
            labels: parameter.labels
          }
          data: parameter.data
        }
---
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
  name: secrets    # it will be the type of component
spec:
  schematic:
    cue:
      template: |
        parameter: {
          // +usage=Name of the ConfigMap
          name: string
          // +usage=Key-value pairs to be stored in the ConfigMap
          data: [string]: string
        }
        output: {
          apiVersion: "v1"
          kind: "Secret"
          metadata: {
            name: parameter.name
          }
          data: parameter.data
        }

now using these ComponentDefinition

components:
    - name: redis-configuration
      type: configmap
      properties:
        name: redis-config
        labels:
          role: redis
        data:
          redis.conf: |-
            bind 0.0.0.0
            port 6379
            tcp-backlog 511
            masterauth OKz6eZYrkIZQSjtFb1
            requirepass OKz6eZYrkIZQSjtFb1
            protected-mode no
            timeout 0
            tcp-keepalive 300
            pidfile /var/run/redis_6379.pid
            loglevel notice
            logfile ""
            dbfilename dump.rdb
            dir /data/
            replica-read-only yes
            appendfsync everysec
# and similarly for the secrets

now you get it how simple the things are

another component of the Redis DB

    - name: redis
      type: statefulservice
      properties:
        replicas: 1
        image: redis:6.2.3-alpine
        cmd: ["redis-server"]
        args: ["/etc/redis/redis.conf"]
        name: redis
        ports:
          - port: 6379
            protocol: TCP
            expose: true
        volumeMounts:
          pvc:
            - name: data
              mountPath: /data
              size: 500Mi
              claimName: redis-pvc
          configMap:
            - name: config
              mountPath: "/etc/redis/redis.conf"
              subPath: redis.conf
              cmName: redis-config

and similarly the http server

    - name: http-server
      type: webservice
      properties:
        image: docker.io/dipugodocker/hackathon-napptive:3.0@sha256:514c510cae88324263c170bc265bd45cf28a605568b90fed0ebb32afb9ead401
        ports:
          - port: 8080
            expose: true
        env:
          - name: REDIS_PORT
            value: "6379"
          - name: REDIS_HOST
            value: "redis-0.redis-headless"
          - name: REDIS_PASSWORD
            valueFrom:
              secretKeyRef:
                name: redis-password
                key: password

      traits: # used to add ingress
        - type: napptive-ingress
          properties:
            port: 8080
            path: /

        - type: scaler # used to have deployment having 2 pods
          properties:
            replicas: 2

so how to manage which one first gets deployed

workflows 🧐

first the config map and secrets then the database and finally the webserver

so here is the final OAM template ready to be deployed

apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
  name: configmap
spec:
  schematic:
    cue:
      template: |
        parameter: {
          // +usage=Name of the ConfigMap
          name: string
          // +usage=Key-value pairs to be stored in the ConfigMap
          data: [string]: string
          // +usage=Key-value pairs for labels
          labels: [string]: string
        }
        output: {
          apiVersion: "v1"
          kind: "ConfigMap"
          metadata: {
            name: parameter.name
            labels: parameter.labels
          }
          data: parameter.data
        }

---
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
  name: secrets
spec:
  schematic:
    cue:
      template: |
        parameter: {
          // +usage=Name of the ConfigMap
          name: string
          // +usage=Key-value pairs to be stored in the ConfigMap
          data: [string]: string
        }
        output: {
          apiVersion: "v1"
          kind: "Secret"
          metadata: {
            name: parameter.name
          }
          data: parameter.data
        }

---
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: hackathon-application
spec:
  components:

    - name: redis-configuration
      type: configmap
      properties:
        name: redis-config
        labels:
          role: redis
        data:
          redis.conf: |-
            bind 0.0.0.0
            port 6379
            tcp-backlog 511
            masterauth OKz6eZYrkIZQSjtFb1
            requirepass OKz6eZYrkIZQSjtFb1
            protected-mode no
            timeout 0
            tcp-keepalive 300
            pidfile /var/run/redis_6379.pid
            loglevel notice
            logfile ""
            dbfilename dump.rdb
            dir /data/
            replica-read-only yes
            appendfsync everysec

    - name: redis-secrets
      type: secrets
      properties:
        name: redis-password
        data:
          password: T0t6NmVaWXJrSVpRU2p0RmIxCg==

    - name: redis
      type: statefulservice
      properties:
        replicas: 1
        image: redis:6.2.3-alpine
        cmd: ["redis-server"]
        args: ["/etc/redis/redis.conf"]
        name: redis
        ports:
          - port: 6379
            protocol: TCP
            expose: true
        volumeMounts:
          pvc:
            - name: data
              mountPath: /data
              size: 500Mi
              claimName: redis-pvc
          configMap:
            - name: config
              mountPath: "/etc/redis/redis.conf"
              subPath: redis.conf
              cmName: redis-config

    - name: http-server
      type: webservice
      properties:
        image: docker.io/dipugodocker/hackathon-napptive:3.0@sha256:514c510cae88324263c170bc265bd45cf28a605568b90fed0ebb32afb9ead401
        ports:
          - port: 8080
            expose: true
        env:
          - name: REDIS_PORT
            value: "6379"
          - name: REDIS_HOST
            value: "redis-0.redis-headless"
          - name: REDIS_PASSWORD
            valueFrom:
              secretKeyRef:
                name: redis-password
                key: password

      traits:
        - type: napptive-ingress
          properties:
            port: 8080
            path: /

        - type: scaler
          properties:
            replicas: 2

  workflows:
    steps:
      - name: redis-config
        type: apply-component
        properties:
          component: redis-configuration
      - name: redis-secrets
        type: apply-component
        properties:
          component: redis-secrets
      - name: redis-server
        type: apply-component
        properties:
          component: redis
      - name: http-server
        type: apply-component
        properties:
          component: http-server

how to deploy this?

Step 1: Click login to playground

Step 2: Click on deploy app

Step 3: Paste the above yaml

🎉You have deployed your first OAM app

click on the endpoint and enjoy the demo

References

Conclusion

By using these concepts, OAM enables developers to focus on writing code and defining components, while operators can focus on managing the deployment and runtime aspects of the application. This separation of concerns helps to streamline the development and operation process, making it easier to build, deploy, and manage cloud-native applications.

I hope you all enjoyed it, if I have made mistake do let me know

Twitter Link

0
Subscribe to my newsletter

Read articles from Dipankar Das directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dipankar Das
Dipankar Das

DevOps, Development and CLoud related stuff Kubesimplify ambassador and a OSS contributor to various CNCF projects