oam-dev / spec

Open Application Model (OAM).
https://oam.dev
Other
3.06k stars 247 forks source link

The Component should mutable #350

Closed resouer closed 4 years ago

resouer commented 4 years ago

Mutable Component Model

OAM v1alpha1 made a requirement that developer need to define another ComponentSchematic with different name for update (i.e. ComponentSchematic is immutable). We've got many feedbacks from real practices in Alibaba as well as AWS ECS for OAM that it's not a good experience. We agreed on solving it in v1alpha2 with new Component + Workload design. But today, we are still claiming that Component is immutable in v1alpha2 spec.

Goal:

Developer should be able to modify the Component object directly to trigger further upgrade of the application. i.e. Component is mutable.

Non-goal:

Revision design around how to track Component change, this should be addressed in: #336

Proposed Change

Change 1

Remove the assumption that a Component could be shared as "template" across multiple AppConfigs.

Sharing a mutable object is essentially problematic as it mixed ownership from multiple sides. If I defined a Component with image:v1, I will be very shocked to see it can be modified by someone else to image:v2 later.

Change 2

The relationship between Component and Workload is not template and instance anymore.

It's important to highlight that what developer maintained is the Component object, it should always represent the desired state of his/her application, and it's an "instance".

Essentially, Component is the envelope to carry Workload with extra fields such as spec.parameters (and spec.polices in the future). Thus Component is 1:1 mapping to Workload CR and they can have same name in implementation.

AppConfig still refer the name of Component which implies its controller always tracks the latest version of Component (i.e. the latest desired state of Workload).

Revision consideration

Revision design is off topic but it worth to mention that revisioning could be fully defer to the implementation layer.

In detail, the implementation can choose/define its own Revision object internally to record revision history of Component. For example, using ControllerRevision of Kubernetes to store the Component spec as revision data.

The cons is if a Trait needs to explicitly refer revisions (e.g. A/B test trait), it will need to refer to the name of that internal Revision object. Of course, this can be fixed by defining a Revision kind in OAM spec for best portability.

Note that developer submitting a Component object only generates a Revision object, it does not generate Workload object until AppConfig is submitted (i.e. still lazy instantiate).

Alternatives

For the relationship of Component and Workload, there're several other options but seems none of them fully meet our goal.

Option 1:

Component is an envelope to generate Workload which reflects the concept of revision.

That is to say, Component is 1:N mapping to Workload. Every time a developer modified the spec.workload of Component, it will generate a Workload object such as: frontend-workload-v1 and frontend-worklod-v2, they could be directly referenced as reversion names in Trait.

But in this case, upon the developer submitted a Component, the system will have to generate Workload object immediately and this will trigger the Workload controller to response. Otherwise, the operator can't know the name of specific Workload in AppConfig later if he need to refer to a specific revision of Component. This breaks the fundamental workflow unless we enforce convention of Workload names like always end with -v{number} suffix. The mind burden is relatively heavy for users though.

Note that there're some PaaS/Serveless projects follow this model, because it can rely on its own Workload controller to determine when to generate the real K8s object (e.g. Deployment) after Workload CR is created. But this does not work for us as Workload controller in OAM could be brought by user.

Option 2:

Add a version field to Component object, so every time a Component is modified, it generates a new version of Component with same name. AppConfig will need to reference Component by name + version.

But in Kubernetes resource model which OAM adopted, it's infeasible as you can't have two objects with same name and same G/V/K. Also, it still have same issue of "shared Component".

Conclusion

The experience of modifying Component name for every update has been proven as unacceptable, and we all agree to fix it. But it does not mean the Component should become a "template" object. In practice, it's impossible to maintain such a shared and user facing object in the system. That experience will be broken.

Hence, this proposal is simple in general, we should claim:

  1. Component is mutable;
  2. Component can't be shared across AppConfigs.

Changes needed:

  1. OAM spec: only documentation change is needed to reflect mutable Component idea, ref: #343;
  2. Crossplane: AppConfig controller will need to watch Component change and response. Component can have same name with Workload (not required but helps to simplify the impl).
resouer commented 4 years ago

/cc @ryanzhang-oss @negz @vturecek @artursouza @hongchaodeng @wonderflow

wonderflow commented 4 years ago

The experience of modifying Component name for every update has been proven as unacceptable. The Component shouldn't become a "template" object.

Totally agree.

This point is really very IMPORTANT and we MUST fix it ASAP.

hongchaodeng commented 4 years ago

This sounds a great idea!

Component should capture all developers' needs and should be modifiable by developers. The revisions should be managed by OAM platform and used by developers to rollback or canary rollout with ease.

resouer commented 4 years ago

Per discussion with @vturecek, it seems that we all agreed Component should become mutable in general.

Though Vac proposed "Manual Gate" before updated really happen instead of "Track Latest" mentioned in previous proposal:

AppConfig still refer the name of Component which implies its controller always tracks the latest version of Component (i.e. the latest desired state of Workload).

"Manual Gate" means after a Component is modified, AppConfig controller will only generate the Workload CR when a revision field is manually updated to point to the newer version in AppConfig.

"Manual Gate" is useful, though the main argument is around default behavior, there're two approaches:

Approach 1: Track Latest by default.

Solution 1: AppConfig have revision field but it's optional.

apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
meta:
  name: my-app
spec:
  components:
    - component: frontend
      revision: 1 # Manual Gate
    - component: backend # Track Latest

Solution 2: no revision field in AppConfig, and rely on a core Trait to control the upgrade behavior (i.e. Manual Gate per Ops requirement).

---
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
meta:
  name: my-app
spec:
  components:
    - component: frontend
      traits:
        - trait:
            apiVersion: core.oam.dev/v1alpha2
            kind: ManualGate
            spec:
              revision: 1
    - component: backend # Track Latest

Approach 2: Manual Gate by default.

Solution: revision field is required in AppConfig, and revision: latest means track latest.

apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
meta:
  name: my-app
spec:
  components:
    - component: frontend
      revision: 1 # Manual Gate
    - component: backend
      revision: latest # Track Latest

In general, Approach 1 has better UI/UX, more flexibility and doesn't need to change Spec a lot, though Approach 2 has stronger workflow control.

artursouza commented 4 years ago

I like approach 2 better because it looks familiar to users from Docker compose.

resouer commented 4 years ago

According to discussion in community meeting (4/21), we agree on Approach 1 (Track Latest by default) for better user experience and backward compatibility.

Though there's argument raised about how Revision will be designed. It's off topic for this issue while some details could be copied and highlighted to here from #336.

Solution: introducing an independent Revision object to track Component change automatically.

apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
  name: frontend
spec:
  workload:
    apiVersion: core.oam.dev/v1alpha2
    kind: ContainerizedWorkload
    metadata:
      name: sample-workload
    spec:
      containers:
      - name: my-cool-workload
        image: example/very-cool-workload:0.1.2@sha256:verytrustworthyhash
        cmd:
        - "bash lscpu"
$ kubectl apply -f component.yaml

Let's use ControllerRevision in K8s as the Revision object in following demo.

A ControllerRevision object will be generated automatically:

$ kubectl get controllerrevisions
NAMESPACE     NAME                  CONTROLLER               REVISION   AGE
default       frontend-c8bb659c5    core.oam.dev/component   1          2d15h

$ kubectl get controllerrevision frontend-c8bb659c5 -o yaml
apiVersion: apps/v1
kind: ControllerRevision
metadata:
  creationTimestamp: "2020-04-19T02:03:27Z"
  labels:
    controller-revision-hash: c8bb659c5
  name: frontend-c8bb659c5
  namespace: default
  ownerReferences:
  - apiVersion: core.oam.dev/v1alpha2
    blockOwnerDeletion: true
    controller: true
    kind: Component
    name: frontend
  resourceVersion: "338"
revision: 1
data:
  spec:
    workload:
      apiVersion: core.oam.dev/v1alpha2
      kind: ContainerizedWorkload
      metadata:
        name: sample-workload
      spec:
        containers:
        - name: my-cool-workload
          image: example/very-cool-workload:0.1.2@sha256:verytrustworthyhash
          cmd:
          - "bash lscpu"

Make a change to component.yaml:

           cmd:
-            - "bash lscpu"
+            - "bash top"

A new ControllerRevision will be automatically generated:

$ kubectl get controllerrevisions
NAMESPACE     NAME                  CONTROLLER               REVISION   AGE
default       frontend-c8bb659c5    core.oam.dev/component   1          2d15h
default       frontend-a75588698    core.oam.dev/component   2          2d14h

$ kubectl get controllerrevision frontend-a75588698 -o yaml
apiVersion: apps/v1
kind: ControllerRevision
metadata:
  creationTimestamp: "2020-04-19T03:03:27Z"
  labels:
    controller-revision-hash: a75588698
  name: frontend-a75588698
  namespace: default
  ownerReferences:
  - apiVersion: core.oam.dev/v1alpha2
    blockOwnerDeletion: true
    controller: true
    kind: Component
    name: frontend
  resourceVersion: "339"
revision: 2
data:
  spec:
    workload:
      apiVersion: core.oam.dev/v1alpha2
      kind: ContainerizedWorkload
      metadata:
        name: sample-workload
      spec:
        containers:
        - name: my-cool-workload
          image: example/very-cool-workload:0.1.2@sha256:verytrustworthyhash
          cmd:
          - "bash top"

The name of ControllerRevision could be referenced by Trait in AppConfig:

apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
metadata:
  name: example-appconfig
spec:
  components:
    - componentName: frontend
      traits:
        - trait:
            apiVersion: core.oam.dev/v1alpha2
            kind: FancyTrait
            spec:
              revision: frontend-c8bb659c5 # or `revision: 1` depends on Trait implementation.

Note that if we allow implementation layer to choose its own Revision definition (e.g. ControllerRevision above), we can achieve above workflow without any change to data structures in OAM spec. Otherwise we need to add a ComponentRevision object in OAM spec.

/cc @artursouza hopes the pseudo workflow above answers your question.

resouer commented 4 years ago

For revision design, there's another alternative to introduce an RevisionTemplate object in Component, so developer can explicitly give name to ControllerRevision instead of always auto-generated.

This is the requirement from @artursouza if I understand correctly.

In that case, the Component will look like:

apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
  name: frontend
spec:
  template:
    metadata:
      name: frontend-v1 # optional, auto generated name by default
    workload:
      apiVersion: core.oam.dev/v1alpha2
      kind: ContainerizedWorkload
      metadata:
        name: sample-workload
      spec:
        containers:
        - name: my-cool-workload
          image: example/very-cool-workload:0.1.2@sha256:verytrustworthyhash
          cmd:
          - "bash lscpu"

Create/update this Component will generate the corresponding ControllerRevision object we mentioned before:

$ kubectl get controllerrevisions
NAMESPACE     NAME           CONTROLLER               REVISION   AGE
default       frontend-v1    core.oam.dev/component   1          2d15h
apiVersion: apps/v1
kind: ControllerRevision
metadata:
  creationTimestamp: "2020-04-19T02:03:27Z"
  labels:
    controller-revision-hash: c8bb659c5
  name: frontend-v1
  namespace: default
  ownerReferences:
  - apiVersion: core.oam.dev/v1alpha2
    blockOwnerDeletion: true
    controller: true
    kind: Component
    name: frontend
  resourceVersion: "338"
revision: 1
data:
  spec:
    workload:
      apiVersion: core.oam.dev/v1alpha2
      kind: ContainerizedWorkload
      metadata:
        name: sample-workload
      spec:
        containers:
        - name: my-cool-workload
          image: example/very-cool-workload:0.1.2@sha256:verytrustworthyhash
          cmd:
          - "bash lscpu"

Note that if the goal is just control of name, I personally think RevisionTemplate is not a strong need. We can define a convention that ControllerRevision is always named as component.name + revision number.

resouer commented 4 years ago

A follow up to https://github.com/oam-dev/spec/issues/350#issuecomment-616952502 :

Since we've brought up Revision design it worth to mention that we can also define component and revisionName as exclusive in AppConfig, so user can make choice without thinking about "what's default".

Approach 3: No default.

apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
meta:
  name: my-app
spec:
  components:
    - componentName: frontend # Track Latest
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
meta:
  name: my-app
spec:
  components:
    - revisionName: frontend-a75588698 # Manual Gate
resouer commented 4 years ago

If there's no objection, according various feedback from community, it seems Approach 3: No default fits to all need and only introduces minimal change to the spec.

wonderflow commented 4 years ago

If there's no objection, according various feedback from community, it seems Approach 3: No default fits to all need and only introduces minimal change to the spec.

Agree, is someone going to send a PR for this minor change?

hongchaodeng commented 4 years ago

I think the general idea and philosophy described here deserves a design/best practice doc. This would guide the incoming implementation in Crossplane.

Let's pull up a design doc and cover an end2end user scenario on how to do upgrade. It might also include implementation details.