Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for Lifecycle Hooks #1171

Closed
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions contributors/design-proposals/lifecycle-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Lifecycle Hooks
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All docs in the design-proposals directly belong in SIG-specific subdirectories

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm gonna move it to KEP; this structure is year old based on what was there at the time it started.


**Author**: @tnozicka

**Status**: Proposal

**RFC Issue**: https://github.com/kubernetes/kubernetes/issues/14512

## Abstract
The intent of this proposal it to support lifecycle hooks in all objects having rolling/recreate update; currently those are Deployments, StatefulSets and DaemonSets.
Lifecycle hooks should allow users to better customize updates (rolling/recreate) without having to write their own controllers.

## Lifecycle Hooks
Lifecycle hook is a Job that will get triggered by the update reaching certain progress point (like 50%).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these generic "lifecycle" hooks or just "update progress" hooks? The naming implies they are hooks that could apply at other points in the lifecycle (scale up, down, thresholds, delete) but they are only used for updates. Fix the naming of the usages?

The progress point is determined by the ratio of new.availableReplicas to the declared replicas or by explicitly stating the number of new.availableReplicas.

### Previous Implementations
Lifecycle hooks are already implemented in OpenShift but we want to enhance them and implement them natively in Kubernetes.
- [OpenShift proposal](https://github.com/openshift/origin/blob/master/docs/proposals/post-deployment-hooks.md)
- [OpenShift docs](https://docs.openshift.org/latest/dev_guide/deployments/deployment_strategies.html#lifecycle-hooks)

### Previous Kubernetes Proposals
- https://github.com/kubernetes/kubernetes/pull/33545

### Use Cases
The most common use case for lifecycle hooks is to have pre, mid and/or post hook. It is mostly used to run some kind of acceptance check in the middle and/or at the end to fully verify the update is working as expected and rollback if it isn't.
The acceptance check may be time consuming and more thorough than what readiness and liveness probes are intended for.
You can also notify external services from them, migrate database in the middle of an update, send messages to IRC channel or do anything else.

[A short demo](https://youtu.be/GVNTm_K43iI) simulating lifecycle hooks using auto-pausing that has been presented at SIG-Apps meeting on August 21, 2017.

#### Reusability
If you are a big shop and you are running several instances of e.g. your database you want to reuse definition of lifecycle hooks e.g. for several instances of your database.
This is reflected in the design bellow by having separate object to define lifecycle hooks and reference it from the objects.

If you would be worried about having shared definitions so e.g. your mistakes won't spread too much you can always choose not to share those definitions and reference unique lifecycle hook objects from every instance.

## API Objects
### New Objects
```go
type LifecycleTemplate struct {
TypeMeta
ObjectMeta
Spec LifecycleTemplateSpec
}

type LifecycleTemplateSpec struct {
// "Always" - keep all Jobs
// "OnFailure" - keep only failed Jobs
// "Never" - delete Jobs immediately
RetainPolicy string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there ever a case where this policy might differ between hooks within the same template?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Different hooks can have different priority (say notifying IRC vs. migrating a database); this way you can choose to retain only some of them.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those use cases seem to provide an argument for retention policy being associated with each LifecycleHook rather than the LifecycleTemplateSpec.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this to per hook then ;)


// After reaching this limit the Job history will be pruned.
RevisionHistoryLimit *int32

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit inflexible given the 1:1 relationship between a LifecycleHook and Job, especially given the size of Hooks is variable. Does it make any sense for the history limit to be per hook?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both options make sense to me; limit hooks history per rollout and per hook.

Other option is to start with no limiting because with owner references it will be automatically limited by the RevisionHistoryLimit from the deployment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other option is to start with no limiting because with owner references it will be automatically limited by the RevisionHistoryLimit from the deployment

Interesting point. I see there was some prior discussion of job pruning.

My current thought is the proposal would be simplified by removing any sort of bespoke pruning capability. Pruning of jobs generally seems orthogonal.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine starting small without any explicit pruning.


// List of lifecycle hooks
// Can have multiple hooks at the same ProgressPoint; order independent
Hooks []LifecycleHook
}

type LifecycleHook struct {
// Unique name
Name string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unique in what scope?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the scope of Hooks []LifecycleHook; likely better to mention the restriction there


// ProgressPoint specifies the point during update when the hook should be triggered.
// Accepts both the number of new pods available or a percentage value representing the ratio
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to implement something like this then isn't autopausing an easier thing to implement and handle arbitrary workflow execution (start a job or really anything else) outside of the core controllers?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to implement something like this then isn't autopausing an easier thing to implement

From the scaling point of view, the amount of work will be slightly less as this will be like setting a partition, only internally, without exposing it in the API (yet). We can as well expose the partition in API (auto-pausing) but I didn't want to mix it into this proposal.

handle arbitrary workflow execution (start a job or really anything else) outside of the core controllers?

I'd prefer to see it in apps. I think this would be beneficial and helpful for the users - I don't want to decouple this and have it only in some distribution just because we can.

// of new.availableReplicas to the declared replicas. In case of getting a percentage
// it will try to reach the exact or the closest possible point right after it,
// trigger the job, wait for it to complete and then continue.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document what happens with edges - 0, "0%", and "100%"

// If such situation shall occur that two different ProgressPoints should be reached at
// the same time, all the hooks for an earlier ProgressPoint will be ran (and finished)
// before any later one.
ProgressPoint intstr.IntOrString

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has anybody yet envisioned another triggering criterion? If we think we might come up with more, a single field coupled to availableReplicas will become messy and we might want to talk about a new type here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a use case not covered by ProgressPoint but interested to hear ideas.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please let's not use this type any more. It was a bad idea. Make individual fields with semantically significant names.


// "Abort" - Failure for this hook is fatal, deployment is considered failed and should be rollbacked.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no such thing as automatic rollback today in Deployments so in terms of what we have the deployment is stuck. If we implement automatic rollback here we should also think of the case where we don't run any hooks (only progressDeadlineSeconds).

// "Ignore" - Even if the hook fails deployment continues rolling out new version.
FailurePolicy string

JobSpec v1.JobSpec
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I parameterize the job spec based on data in my deployment? Or would I have to create one hook per deployment in that case?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan was to inject some environment variables inside (like image) but I failed to mention it in the proposal

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way for downward API to help here? The platform choosing a static and arbitrary set of fields to expose as (arbitrary) env vars is definitely useful, but allowing the users to select what data to expose and as mounted files seems very interesting to me. Crazy?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of extending downward API to cover this use case where the objects are separate. That would allow you to pass any deployment property and we could inject just limited and necessary number of envs like the previous image or similar.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of extending downward API to cover this use case where the objects are separate. That would allow you to pass any deployment property

Expanding downward API to be cross-object is a pretty big change... is that desired? If so, we'd need to be really careful:

  • puts a burden on the kubelet to watch more objects
  • could get the kubelet to fetch and inject info you shouldn't be able to see
  • can run into ordering problems when multiple unsynchronized data sources update (current downward API info all comes from the pod object which has a linearizable resourceVersion)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reusing existing downward API could be a good argument for having the PolicyTemplate embedded in the object (Deployment, ...) at the expense of re-usability.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reusing existing downward API could be a good argument for having the PolicyTemplate embedded in the object (Deployment, ...) at the expense of re-usability.

Talked some with @tnozicka about this offline and I'm now convinced that the lifecycle hook makes more sense embedded into the deployment spec itself rather than as a reference. Being able to use the downward API for the job template seems more generally useful than reusing hook templates across deployments. Deployments themselves (and any hooks defined within) could be templated in other ways.

}

type LifecycleHookStatus struct {
// Name of the hook
Name string

ProgressPoint intstr.IntOrString

// States: "Running", "Succeeded", "Failed"
State string

// Reference to locate the Job created when executing the hook
JobRef LocalObjectReference
}

type LifecycleHookRevisionStatus struct {
// Revision of the object that this hook was run for
Revision string

LifecycleHookStatuses []LifecycleHookStatus
}
```

### Affected Objects
As of now the lifecycle hooks are relevant for Deployments, StatefulSets and DaemonSets.
They will require new optional field to be able to reference LifecycleTemplate and extending its status by []LifecycleHookRevisionStatus.
Also the controller will need to be slightly adjusted to scale in appropriate chunks and trigger the hooks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think that we might start with Deployment (its the most utilized and generally its used for stateless non-critical components), promote a stable implementation there, and then adapt that implementation to StatefulSet and DaemonSet?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, my plan is to start with Deployments.

```go
type DeploymentSpec struct {
// ...
// Addition
LifecycleTemplate *ObjectReference
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I want to control the rollout process? Isn't that a job itself?

Copy link
Contributor

@0xmichalis 0xmichalis Oct 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a separate strategy that forces the controller to ignore the deployment. Wouldn't that process need to implement the hooks api? Sounds like a separate design proposal.

}

type DeploymentStatus struct {
// ...
// Addition
LifecycleHookRevisionStatuses []LifecycleHookRevisionStatus
}
```

```go
type StatefulSetSpec struct {
// ...
// Addition
LifecycleTemplate *ObjectReference
}

type StatefulSetStatus struct {
// ...
// Addition
LifecycleHookRevisionStatuses []LifecycleHookRevisionStatus
}

```

```go
type DaemonSetSpec struct {
// ...
// Addition
LifecycleTemplate *ObjectReference
}

type DaemonSetStatus struct {
// ...
// Addition
LifecycleHookRevisionStatuses []LifecycleHookRevisionStatus
}
```

## Algorithm
If there is a LifecycleTemplate referenced from an object:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an owner reference?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is an explicit reference from DeploymentSpec, StatefulSetSpec and DaemonSetSpec - LifecycleTemplate *ObjectReference


1. Calculate next partition point to reach the closest lifecycle hook progress point and scale replicas in update appropriately. If there is no hook remaining, GOTO 5.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Does the controller block until the Job completes?
  2. How do you determine if a Job exists for a particular execution of the rolling update? That is, it seems to me that the execution of a Job is tied to the roll out of a specific revision, how do we track the lifecycle of the an individual Job for each roll out and rollback of a particular revision?
  3. Do we require the Jobs to be idempotent for safety. I think it will be difficult to ensure at most once execution. We could probably ensure at least once.
  4. If we are using available replicas as the trigger, what happens if the number of available replicas decreases during execution of the Job?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some concrete examples for 3:

  1. running a database migration - should be reentrant
  2. clearing a cache - should be reentrant
  3. notifying an external system of our current location - should be reentrant

I think at least once is probably ok, but I don't know that all users would expect at-least-once. Similar to daemonset discussion - will users naturally assume these are at-most-once, and if so, will they experience catastrophic failure due to it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of the examples from the previous comment, I think all of them imply the controller MUST block until the job completes. Not running a database migration could be catastrophic.

Copy link
Author

@tnozicka tnozicka Oct 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Does the controller block until the Job completes?

Yes. (Although there is always possibility of adding an option to customize it.)

  1. Do we require the Jobs to be idempotent for safety. I think it will be difficult to ensure at most once execution. We could probably ensure at least once.

User will get whatever guarantees Kubernetes Job gives him. (But it should be idempotent.)

  1. If we are using available replicas as the trigger, what happens if the number of available replicas decreases during execution of the Job?

Nothing, once we reach certain progress point we trigger the hook and never go back.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to think about details of rollback in case of 2. more but it should be based on revision

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you determine if a Job exists for a particular execution of the rolling update? That is, it seems to me that the execution of a Job is tied to the roll out of a specific revision, how do we track the lifecycle of the an individual Job for each roll out and rollback of a particular revision?

Would this be the same mechanism we use to determine whether a replicaset exists for a given deployment? That is, a deterministically named job based on the pod template spec hash.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could associate all the hooks (Jobs) by setting controllerRef to particular RS. If we decide to run hooks for rollbacks, those could be distinguished by label.

We would have probably done it either way for purposes of adoption and garbage collection.

2. When partition point is reached, run the hook by creating a Job using LifecycleHook.JobSpec
3.
1. If the hook failed and FailurePolicy is Abort - emit event, fail the update and initiate rollback (GOTO 5.)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define rollback here. I'm not sure whether you mean "give up on the new deployment and mutate the deployment back to the old revision" or "go into very long backoff". I.e. perm-fail vs not-perm fail

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean fail this rollout for good and rollback to previous (working) revision

2. If the hook failed and FailurePolicy is Ignore - only emit event
4. GOTO 1.
5. Finish

(In case of rollover cancel any hooks running and don't execute new ones.)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by rollover?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


This can be applied to all the existing update strategies as that's essentially dependent only on the ability to have a certain ratio of new.availableReplicas to the declared replicas and running the hook between changing that ratio.