From 4ee69f36ed8272b3c47692d76c6bbbab822ca3f1 Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Tue, 27 Mar 2018 15:14:54 -0700 Subject: [PATCH] Commit API spec and design doc (#444) Initial copy/commit of Elafros API design doc, chunked into sections. Also includes initial definitions of Elafros personas. Still TODO: - Appendices A-E, which cover extended use cases. --- product/personas.md | 96 ++++ spec/errors.md | 322 +++++++++++ spec/images/auto_rollout.png | Bin 0 -> 13330 bytes spec/images/build_example.png | Bin 0 -> 13941 bytes spec/images/build_function.png | Bin 0 -> 14738 bytes spec/images/initial_creation.png | Bin 0 -> 10811 bytes spec/images/manual_rollout.png | Bin 0 -> 17399 bytes spec/images/object_model.png | Bin 0 -> 11425 bytes spec/motivation.md | 22 + spec/normative_examples.md | 929 +++++++++++++++++++++++++++++++ spec/overview.md | 86 +++ spec/spec.md | 263 +++++++++ 12 files changed, 1718 insertions(+) create mode 100644 product/personas.md create mode 100644 spec/errors.md create mode 100644 spec/images/auto_rollout.png create mode 100644 spec/images/build_example.png create mode 100644 spec/images/build_function.png create mode 100644 spec/images/initial_creation.png create mode 100644 spec/images/manual_rollout.png create mode 100644 spec/images/object_model.png create mode 100644 spec/motivation.md create mode 100644 spec/normative_examples.md create mode 100644 spec/overview.md create mode 100644 spec/spec.md diff --git a/product/personas.md b/product/personas.md new file mode 100644 index 000000000..910c31812 --- /dev/null +++ b/product/personas.md @@ -0,0 +1,96 @@ +# Elafros Personas + +When discussing user actions, it is often helpful to [define specific +user roles](https://en.wikipedia.org/wiki/Persona_(user_experience)) who +might want to do the action. + + +## Elafros Compute + +### Developer Personas + +The developer personas are software engineers looking to build and run +a stateless application without concern about the underlying +infrastructure. + +* Hobbyist +* Backend SWE +* Full stack SWE +* SRE + +User stories: +* Deploy some code +* Update environment +* Roll back the last change +* Debug an error in code +* Monitor my application + +### Operator Personas + +* Hobbyist / Contributor +* Cluster administrator +* Security Engineer / Auditor +* Capacity Planner + +User stories: +* Create an Elafros cluster +* Apply policy / RBAC +* Control or charge back for resource usage +* Choose logging or monitoring plugins + + +## Elafros Build + +We expect the build components of Elafros to be useful on their own, +as well as in conjunction with the compute components. + +### Developer + +User stories: +* Start a build +* Read build logs + +### Language operator / contributor + +User stories: +* Create a build image / build pack + + +## Elafros Events + +Event generation and consumption is a core part of the serverless +(particularly function as a service) computing model. Event generation +and dispatch enables decoupling of event producers from consumers. + +## Event consumer (developer) + +User stories: +* Determine what event sources are available +* Trigger my service when certain events happen (event binding) +* Filter events from a provider + +## Event producer + +User stories: +* Publish events +* Control who can bind events + + +## Contributors + +Contributors are an important part of the Elafros project. As such, we +will also consider how various infrastructure encourages and enables +contributors to the project, as well as the impact on end-users. + +* Hobbyist or newcomer +* Motivated user +* Corporate (employed) maintainer +* Consultant + +User stories: +* Check out the code +* Build and run the code +* Run tests +* View test status +* Run performance tests + diff --git a/spec/errors.md b/spec/errors.md new file mode 100644 index 000000000..5f0f3badf --- /dev/null +++ b/spec/errors.md @@ -0,0 +1,322 @@ +# Error Conditions and Reporting + +Elafros uses the standard Kubernetes API pattern for reporting +configuration errors and current state of the system by writing the +report in the `status` section. There are two mechanisms commonly used +in status: + +* conditions represent true/false statements about the current state + of the resource. + +* other fields may provide status on the most recently retrieved state + of the system as it relates to the resource (example: number of + replicas or traffic assignments). + +Both of these mechanisms often include additional data from the +controller such as `observedGeneration` (to determine whether the +controller has seen the latest updates to the spec). Example user and +system error scenarios are included below along with how the status is +presented to CLI and UI tools via the API. + +* [Revision failed to become Ready](#revision-failed-to-become-ready) +* [Build failed](#build-failed) +* [Revision not found by Route](#revision-not-found-by-route) +* [Configuration not found by Route](#configuration-not-found-by-route) +* [Latest Revision of a Configuration deleted](#latest-revision-of-a-configuration-deleted) +* [Resource exhausted while creating a revision](#resource-exhausted-while-creating-a-revision) +* [Deployment progressing slowly/stuck](#deployment-progressing-slowly-stuck) +* [Traffic shift progressing slowly/stuck](#traffic-shift-progressing-slowly-stuck) +* [Container image not present in repository](#container-image-not-present-in-repository) +* [Container image fails at startup on Revision](#container-image-fails-at-startup-on-revision) + + +## Revision failed to become Ready + +If the latest Revision fails to become `Ready` for any reason within some reasonable +timeframe, the Configuration should signal this +with the `LatestRevisionReady` status, copying the reason and the message +from the `Ready` condition on the Revision. + +```yaml +... +status: + latestReadyRevisionName: abc + latestCreatedRevisionName: bcd # Hasn't become "Ready" + conditions: + - type: LatestRevisionReady + status: False + reason: ContainerMissing + message: "Unable to start because container is missing and build failed." +``` + + +## Build failed + +If the Build steps failed while creating a Revision, you can examine +the `Failed` condition on the Build or the `BuildFailed` condition on +the Revision (which copies the value from the build referenced by +`spec.buildName`). In addition, the Build resource (but not the +Revision) should have a status field to link to the log output of the +build. + +```http +GET /apis/build.dev/v1alpha1/namespaces/default/builds/build-1acub3 +``` +```yaml +... +status: + # Link to log stream; could be ELK or Stackdriver, for example + buildLogsLink: "http://logging.infra.mycompany.com/...?filter=..." + conditions: + - type: Failed + status: True + reason: BuildStepFailed # could also be SourceMissing, etc + # reason is a short status, message provides error details + message: "Step XYZ failed with error message: $LASTLOGLINE" +``` + + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + conditions: + - type: Ready + status: False + reason: ContainerMissing + message: "Unable to start because container is missing and build failed." + - type: BuildFailed + status: True + reason: BuildStepFailed + # reason is a short status, message provides error details + message: "Step XYZ failed with error message: $LASTLOGLINE" +``` + + +## Revision not found by Route + +If a Revision is referenced in the Route's `spec.rollout.traffic`, the +corresponding entry in the `status.traffic` list will be set to "Not +found", and the `TrafficDropped` condition will be marked as True, +with a reason of `RevisionMissing`. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/abc +``` +```yaml +... +status: + traffic: + - revisionName: abc + name: current + percent: 100 + - revisionName: "Not found" + name: next + percent: 0 + conditions: + - type: RolloutInProgress + status: False + - type: TrafficDropped + status: True + reason: RevisionMissing + # reason is a short status, message provides error details + message: "Revision 'qyzz' referenced in rollout.traffic not found" +``` + + +## Configuration not found by Route + +If a Route references the `latestReadyRevisionName` of a Configuration +and the Configuration cannot be found, the corresponding entry in +`status.traffic` list will be set to "Not found", and the +`TrafficDropped` condition will be marked as True with a reason of +`ConfigurationMissing`. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/abc +``` +```yaml +... +status: + traffic: + - revisionName: "Not found" + percent: 100 + conditions: + - type: RolloutInProgress + status: False + - type: TrafficDropped + status: True + reason: ConfigurationMissing + # reason is a short status, message provides error details + message: "Revision 'my-service' referenced in rollout.traffic not found" +``` + + +## Latest Revision of a Configuration deleted + +If the most recent (or most recently ready) Revision is deleted, the +Configuration will clear the `latestReadyRevisionName`. If the +Configuration is referenced by a Route, the Route will set the +`TrafficDropped` condition with reason `RevisionMissing`, as above. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service +``` +```yaml +... +metadata: + generation: 1234 # only updated when spec changes + ... +spec: + ... +status: + latestCreatedRevision: abc + conditions: + - type: LatestRevisionReady + status: False + reason: RevisionMissing + message: "The latest Revision appears to have been deleted." + observedGeneration: 1234 +``` + + +## Resource exhausted while creating a revision + +Since a Revision is only metadata, the Revision will be created, but +will have a condition indicating the underlying failure, possibly +indicating the failed underlying resource. In a multitenant +environment, the customer might not have have access or visibility +into the underlying resources in the hosting environment. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + conditions: + - type: Ready + status: False + reason: NoDeployment + message: "The controller could not create a deployment named ela-abc-e13ac." +``` + + +## Deployment progressing slowly/stuck + +See +[the kubernetes documentation for how this is handled for Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#failed-deployment). For +Revisions, we will start by assuming a single timeout for deployment +(rather than configurable), and report that the Revision was not +Ready, with a reason `ProgressDeadlineExceeded`. Note that we will +only report `ProgressDeadlineExceeded` if we could not determine +another reason (such as quota failures, missing build, or container +execution failures). + +Kubernetes controllers will continue attempting to make progress +(possibly at a less-aggressive rate) when they encounter a case where +the desired status cannot match the actual status, so if the +underlying deployment is slow, it might eventually finish after +reporting `ProgressDeadlineExceeded`. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + conditions: + - type: Ready + status: False + reason: ProgressDeadlineExceeded + message: "Unable to create pods for more than 120 seconds." +``` + + +## Traffic shift progressing slowly/stuck + +Similar to deployment slowness, if the transfer of traffic (either via +gradual or abrupt rollout) takes longer than a certain timeout to +complete/update, the `RolloutInProgress` condition will remain at +True, but the reason will be set to `ProgressDeadlineExceeded`. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/abc +``` +```yaml +... +status: + traffic: + - revisionName: abc + percent: 75 + - revisionName: def + percent: 25 + conditions: + - type: RolloutInProgress + status: True + reason: ProgressDeadlineExceeded + # reason is a short status, message provides error details + message: "Unable to update traffic split for more than 120 seconds." +``` + + +## Container image not present in repository + +Revisions might be created while a Build is still creating the +container image or uploading it to the repository. If the build is +being performed by a CRD in the cluster, the spec.buildName attribute +will be set (and see the [Build failed](#build-failed) example). In +other cases when the build is not supplied, the container image +referenced might not be present in the registry (either because of a +typo or because it was deleted). In this case, the Ready condition +will be set to False with a reason of ContainerMissing. This condition +could be corrected if the image becomes available at a later time. We +can also make a defensive copy of the container image to avoid this +error due to deleted source container. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + conditions: + - type: Ready + status: False + reason: ContainerMissing + message: "Unable to fetch image 'gcr.io/...': " + - type: Failed + status: True + reason: ContainerMissing + message: "Unable to fetch image 'gcr.io/...': " +``` + + +## Container image fails at startup on Revision + +Particularly for development cases with interpreted languages like +Node or Python, syntax errors or the like might only be caught at +container startup time. For this reason, implementations may choose to +start a single copy of the container on deployment, before making the +container Ready. If the initial container fails to start, the `Ready` +condition will be set to False and the reason will be set to +`ExitCode:%d` with the exit code of the application, and the last line +of output in the message. Additionally, the Revision will include a +`logsUrl` which provides the address of an endpoint which can be used to +fetch the logs for the failed process. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + logUrl: "http://logging.infra.mycompany.com/...?filter=revision=abc&..." + conditions: + - type: Ready + status: False + reason: ExitCode:127 + message: "Container failed with: SyntaxError: Unexpected identifier" +``` diff --git a/spec/images/auto_rollout.png b/spec/images/auto_rollout.png new file mode 100644 index 0000000000000000000000000000000000000000..04dfb62c4fd8f70d99775f5bb8a206fcd17928fa GIT binary patch literal 13330 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 YlAO`y7}5aXe|@fLsOTtHDcXenFD*|-^#A|> literal 0 HcmV?d00001 diff --git a/spec/images/build_example.png b/spec/images/build_example.png new file mode 100644 index 0000000000000000000000000000000000000000..28fb83e2e3bbdcf569d2df8fd49e9ada4f3b71e6 GIT binary patch literal 13941 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±*GTSm;-ArDQOdcwEH>*sX6 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 T_1(xoi~^>it6r-5F!=ufD|H8N literal 0 HcmV?d00001 diff --git a/spec/images/object_model.png b/spec/images/object_model.png new file mode 100644 index 0000000000000000000000000000000000000000..c48a4df5329c6d183e873ed86715714672c4b762 GIT binary patch literal 11425 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

Ax*)Dc@8FCtG11Py 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 QfwmwWEkn%`^_xNe4Os&?&j0`b literal 0 HcmV?d00001 diff --git a/spec/motivation.md b/spec/motivation.md new file mode 100644 index 000000000..573e6da9b --- /dev/null +++ b/spec/motivation.md @@ -0,0 +1,22 @@ +The goal of the Elafros project is to provide a common toolkit and API +framework for serverless workloads. + +We define serverless workloads as computing workloads that are: + +* Stateless +* Amenable to the process scale-out model +* Primarily driven by application level (L7 -- HTTP, for example) + request traffic + +While Kubernetes provides basic primitives like Deployment, Service, +and Ingress in support of this model, our experience suggests that a +more compact and richer opinionated model has substantial benefit for +developers. In particular, by standardizing on higher-level primitives +which perform substantial amounts of automation of common +infrastructure, it should be possible to build consistent toolkits +that provide a richer experience than updating yaml files with +`kubectl`. + +The Elafros APIs consist of Compute API (these documents), +[Build API](https://github.com/elafros/build) and +[Eventing API](https://github.com/elafros/eventing). diff --git a/spec/normative_examples.md b/spec/normative_examples.md new file mode 100644 index 000000000..8b2aac4bf --- /dev/null +++ b/spec/normative_examples.md @@ -0,0 +1,929 @@ +# Sample API Usage + +Following are several normative sample scenarios utilizing the Elafros +API. These scenarios are arranged to provide a flavor of the API and +building from the smallest, most frequent operations. + +Examples in this section illustrate: + +* [Automatic rollout of a new Revision to an existing Service with a + pre-built container](#1--automatic-rollout-of-a-new-revision-to-existing-service---pre-built-container) +* [Creating a first route to deploy a first revision from a pre-built + container](#2--creating-route-and-deploying-first-revision---pre-built-container) +* [Configuration changes and manual rollout + options](#3--manual-rollout-of-a-new-revision---config-change-only) +* [Creating a revision from source](#4--deploy-a-revision-from-source) +* [Creating a function from source](#5--deploy-a-function) + +Note that these API operations are identical for both app and function +based services. (to see the full resource definitions, see the +[Resource YAML Definitions](spec.md)). + +CLI samples are for illustrative purposes, and not intended to +represent final CLI design. + +## 1) Automatic rollout of a new Revision to existing Service - pre-built container + +**_Scenario_**: User deploys a new revision to an existing service +with a new container image, rolling out automatically to 100% + +``` +$ elafros deploy --service my-service + Deploying app to service [my-service]: +✓ Starting +✓ Promoting + Done. + Deployed to https://my-service.default.mydomain.com +``` + +**Steps**: + +* Update the Configuration with the config change + +**Results:** + +* A new Revision is created, and automatically rolled out to 100% once + ready + +![Automatic Rollout](images/auto_rollout.png) + + +After the initial Route and Configuration have been created (which is +shown in the [second example](TODO)), the typical +interaction is to update the revision configuration, resulting in the +creation of a new revision, which will be automatically rolled out by +the route. Revision configuration updates can be handled as either a +PUT or PATCH operation: + +* Optimistic concurrency controls for PUT operations in a + read/modify/write routine work as expected in kubernetes. + +* PATCH semantics should work as expected in kubernetes, but may have + some limitations imposed by CRDs at the moment. + +In this and following examples PATCH is used. Revisions can be built +from source, which results in a container image, or by directly +supplying a pre-built container, which this first scenario +illustrates. The example demonstrates the PATCH issued by the client, +followed by several GET calls to illustrate each step in the +reconciliation process as the system materializes the new revision, +and begins shifting traffic from the old revision to the new revision. + +The client PATCHes the configuration's template revision with just the +new container image, inheriting previous configuration from the +configuration: + +```http +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service # by convention, same name as the service +spec: + revisionTemplate: # template for building Revision + spec: + container: + image: gcr.io/... # new image +``` + +The update to the Configuration triggers a new revision being created, +and the Configuration is updated to reflect the new Revision: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service + generation: 1235 + ... + +spec: + ... # same as before, except new container.image +status: + latestReadyRevisionName: abc + latestCreatedRevisionName: def # new revision created, but not ready yet + observedGeneration: 1235 +``` + +The newly created revision has the same config as the previous +revision, but different code. Note the generation label reflects the +new generation of the configuration (1235), indicating the provenance +of the revision: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/def +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: def + labels: + elafros.dev/configuration: my-service + elafros.dev/configurationGeneration: 1235 + ... +spec: + container: # k8s core.v1.Container + image: gcr.io/... # new container + # same config as previous revision + env: + - name: FOO + value: bar + - name: HELLO + value: blurg + ... +status: + conditions: + - type: Ready + status: True +``` + +When the new revision is Ready, i.e. underlying resources are +materialized and ready to serve, the configuration updates its +`status.latestReadyRevisionName` status to reflect the new +revision. The route, which is configured to automatically rollout new +revisions from the configuration, watches the configuration and is +notified of the `latestReadyRevisionName`, and begins migrating traffic +to it. During reconciliation, traffic may be routed to both existing +revision `abc` and new revision `def`: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + ... +spec: + rollout: + traffic: + - configurationName: my-service + percent: 100 + +status: + # domain: + # oss: my-service.namespace.mydomain.com + domain: my-service.namespace.mydomain.com + # percentages add to 100 + traffic: # in status, all configurationName refs are dereferenced + - revisionName: abc + percent: 75 + - revisionName: def + percent: 25 + conditions: + - type: RolloutComplete + status: False +``` + +And once reconciled, revision def serves 100% of the traffic : + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + ... +spec: + rollout: + traffic: + - configurationName: my-service + percent: 100 +status: + domain: my-service.default.mydomain.com + traffic: + - revisionName: def + percent: 100 + conditions: + - type: RolloutComplete + status: True + ... +``` + + +## 2) Creating Route and deploying first Revision - pre-built container + +**Scenario**: User creates a new Route and deploys their first + Revision based on a pre-built container + +``` +$ elafros deploy --service my-service --region us-central1 +✓ Creating service [my-service] in region [us-central1] + Deploying app to service [my-service]: +✓ Uploading [=================] +✓ Starting +✓ Promoting + Done. + Deployed to https://my-service.default.mydomain.com +``` + +**Steps**: + +* Create a new Configuration and a Route that references a that + configuration. + +**Results**: + +* A new Configuration is created, and generates a new Revision based + on the configuration + +* A new Route is created, referencing the configuration + +* The route begins serving traffic to the revision that was created by + the configuration + +![Initial Creation](images/initial_creation.png) + + +The previous example assumed an existing Route and Configuration to +illustrate the common scenario of updating the configuration to deploy +a new revision to the service. + +In this getting started example, deploying a first Revision is +accomplished by creating a new Configuration (which will generate a +new Revision) and creating a new Route referring to that +configuration. Note that these two steps can occur in either order, or +in parallel. + +A Route can either refer directly to a Revision, or to the latest +ready revision of a Configuration, as this example illustrates. This +is the most straightforward scenario that many Elafros customers are +expected to use, and is consistent with the experience of deploying +code that is rolled out immediately. + +The example shows the POST calls issued by the client, followed by +several GET calls to illustrate each step in the reconciliation +process as the system materializes and begins routing traffic to the +revision. + +The client creates the route and configuration, which by convention +share the same name: + +```http +POST /apis/elafros.dev/v1alpha1/namespaces/default/routes +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + traffic: + - configurationName: my-service # named reference to Configuration + percent: 100 # automatically activate new Revisions from the configuration +``` + +```http +POST /apis/elafros.dev/v1alpha1/namespaces/default/configurations +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service # By convention (not req'd), same name as the service. + # This will also be set as the "elafros.dev/configuration" + # label on the created Revision. +spec: + revisionTemplate: # template for building Revision + metadata: ... + spec: + container: # k8s core.v1.Container + image: gcr.io/... + env: + - name: FOO + value: bar + - name: HELLO + value: world + ... +``` + +Upon the creation of the configuration, the system will create a new +Revision, generating its name, and applying the spec and metadata from +the configuration, as well as new metadata labels: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: abc # generated name + labels: + # name and generation of the configuration that created the revision + elafros.dev/configuration: my-service + elafros.dev/configurationGeneration: 1234 + ... # uid, resourceVersion, creationTimestamp, generation, selfLink, etc +spec: + ... # spec from the configuration +status: + conditions: + - type: Ready + status: False + message: "Starting Instances" +``` + +Immediately after the revision is created, i.e. before underlying +resources have been fully materialized, the configuration is updated +with latestCreatedRevisionName: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service + generation: 1234 + ... # uid, resourceVersion, creationTimestamp, selfLink, etc +spec: + ... # same as before +status: + # latest created revision, may not have materialized yet + latestCreatedRevisionName: abc + observedGeneration: 1234 +``` + +The configuration watches the revision, and when the revision is +updated as Ready (to serve), the latestReadyRevisionName is updated: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service + generation: 1234 + ... +spec: + ... # same as before +status: + # the latest created and ready to serve. Watched by service + latestReadyRevisionName: abc + # latest created revision + latestCreatedRevisionName: abc + observedGeneration: 1234 +``` + +The route, which watches the configuration `my-service`, observes the +change to `latestReadyRevisionName` and begins routing traffic to the +new revision `abc`, addressable as +`my-service.default.mydomain.com`. Once reconciled: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + generation: 2145 + ... +spec: + rollout: + traffic: + - configurationName: my-service + percent: 100 + +status: + domain: my-service.default.mydomain.com + + traffic: # in status, all configurationName refs are dereferenced to latest revision + - revisionName: abc # latestReadyRevisionName from configurationName in spec + percent: 100 + + conditions: + - type: RolloutComplete + status: True + + observedGeneration: 2145 +``` + + +## 3) Manual rollout of a new Revision - config change only + +**_Scenario_**: User updates configuration with new configuration (env + var change) to an existing service, tests the revision, then + proceeds with a manually controlled rollout to 100% + +``` +$ elafros rollout strategy manual + +$ elafros deploy --service my-service --env HELLO="blurg" +[...] + +$ elafros revisions list --service my-service +Name Traffic Id Date Deployer Git SHA +next 0% v3 2018-01-19 12:16 user1 a6f92d1 +current 100% v2 2018-01-18 20:34 user1 a6f92d1 + v1 2018-01-17 10:32 user1 33643fc + +$ elafros rollout next percent 5 +[...] +$ elafros rollout next percent 50 +[...] +$ elafros rollout finish +[...] + +$ elafros revisions list --service my-service +Name Traffic Id Date Deployer Git SHA +current,next 100% v3 2018-01-19 12:16 user1 a6f92d1 + v2 2018-01-18 20:34 user1 a6f92d1 + v1 2018-01-17 10:32 user1 33643fc +``` + +**Steps**: + +* Update the Route to pin the current revision + +* Update the Configuration with the new configuration (env var) + +* Update the Route to address the new Revision + +* After testing the new revision through the named subdomain, proceed + with the rollout, incrementally increasing traffic to 100% + +**Results:** + +* The system creates the new revision from the configuration, + addressable at next.my-service... (by convention), but traffic is + not routed to it until the percentage is manually ramped up. Upon + completing the rollout, the next revision is now the current + revision + +![Manual rollout](images/manual_rollout.png) + + +In the previous examples, the route referenced a Configuration for +automatic rollouts of new Revisions. While this pattern is useful for +many scenarios such as functions-as-a-service and simple development +flows, the Route can also reference Revisions directly to "pin" +traffic to specific revisions, which is suitable for manually +controlling rollouts, i.e. testing a new revision prior to serving +traffic. (Note: see [Appendix B](complex_examples.md) for a +semi-automatic variation of manual rollouts). + +The client updates the route to pin the current revision: + +```http +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + traffic: + - revisionName: def # pin a specific revision, i.e. the current one + percent: 100 +``` + +As in the previous example, the configuration is updated to trigger +the creation of a new revision, in this case updating the container +image but keeping the same config: + +```http +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service +spec: + revisionTemplate: + spec: + container: + env: # k8s-style strategic merge patch, updating a single list value + - name: HELLO + value: blurg # changed value +``` + +A new revision `ghi` is created that has the same code as the previous +revision `def`, but different config: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/ghi +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: ghi + ... +spec: + container: + image: gcr.io/... # same container as previous revision abc + env: + - name: FOO + value: bar + - name: HELLO + value: blurg # changed value + ... +status: + conditions: + - type: Ready + status: True +``` + +Even when ready, the new revision does not automatically start serving +traffic, as the route was pinned to revision `def`. + +Update the route to make the existing revision serving traffic +addressable through subdomain `current`, and referencing the new +revision at 0% traffic but making it addressable through subdomain +`next`: + +```http +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + traffic: + - revisionName: def + name: current # addressable as current.my-service.default.mydomain.com + percent: 100 + - revisionName: ghi + name: next # addressable as next.my-service.default.mydomain.com + percent: 0 # no traffic yet +``` + +In this state, the route makes both revisions addressable with +subdomains `current` and `next` (once the revision `ghi` has a status of +Ready), but traffic has not shifted to next yet. Also note that while +the names current/next have semantic meaning, they are convention +only; blue/green, or any other subdomain names could be configured. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + ... +spec: + ... # unchanged +status: + domain: my-service.default.mydomain.com + traffic: + - revisionName: def + name: current # addressable as current.my-service.default.mydomain.com + percent: 100 + - revisionName: ghi + name: next # addressable as next.my-service.default.mydomain.com + percent: 0 + conditions: + - type: RolloutComplete + status: True + ... +``` + +After testing the new revision at +`next.my-service.default.mydomain.com`, it can be rolled out to 100% +(either directly, or through several increments, with the split +totaling 100%): + +```http +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + Traffic: # percentages must total 100% + - revisionName: def + name: current + percent: 0 + - revisionName: ghi + name: next + percent: 100 # migrate traffic fully to the next revision +``` + +After reconciliation, all traffic has been shifted to the new version: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + ... +spec: + ... # unchanged +status: + domain: my-service.default.mydomain.com + traffic: + - revisionName: def + name: current + percent: 0 + - revisionName: ghi + name: next + percent: 100 + conditions: + - type: RolloutComplete + status: True + ... +``` + +By convention, the final step when completing the rollout is to update +`current` to reflect the new revision. `next` can either be removed, or +left addressing the same revision as current so that +`next.my-service.default.mydomain.com` is always addressable. + +```http +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + traffic: + - revisionName: ghi # update for the next rollout, current = next + name: current + percent: 100 + - revisionName: ghi # optional: leave next as also referring to ghi + name: next + percent: 0 +``` + + +## 4) Deploy a Revision from source + +**Scenario**: User deploys a revision to an existing service from + source rather than a pre-built container + +``` +$ elafros deploy --service my-service + Deploying app to service [my-service]: +✓ Uploading [=================] +✓ Detected [node-8-9-4] runtime +✓ Building +✓ Starting +✓ Promoting + Done. + Deployed to https://my-service.default.mydomain.com +``` + +**Steps**: + +* Create/Update a Configuration, inlining build details. + +**Results**: + +* The Configuration is created/updated, which generates a container + build and a new revision based on the template, and can be rolled + out per earlier examples + +![Build Example](images/build_example.png) + + +Previous examples demonstrated configurations created with pre-built +containers. Revisions can also be created by providing build +information to the configuration, which results in a container image +built by the system. The build information is supplied by inlining the +BuildSpec of a Build resource in the Configuration. This describes: + +* **What** to build (`build.source`): Source can be provided as an + archive, manifest file, or repository. + +* **How** to build (`build.template`): a + [BuildTemplate](https://github.com/elafros/build) is referenced, + which describes how to build the container via a builder with + arguments to the build process. + +* **Where** to publish (`build.template.arguments`): Image registry + url and other information specific to this build invocation. + +The client creates the configuration inlining a build spec for an +archive based source build, and referencing a nodejs build template: + +```http +POST /apis/elafros.dev/v1alpha1/namespaces/default/configurations +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service +spec: + build: # build.dev/v1alpha1.BuildTemplateSpec + source: + # oneof git|gcs|custom: + git: + url: https://... + commit: ... + template: # defines build template + name: nodejs_8_9_4 # builder name + namespace: build-templates + arguments: + - name: _IMAGE + value: gcr.io/... # destination for image + + revisionTemplate: # template for building Revision + metadata: ... + spec: + container: # k8s core.v1.Container + image: gcr.io/... # Promise of a future build. Same as supplied in + # build.template.arguments[_IMAGE] + env: # Updated environment variables to go live with new source. + - name: FOO + value: bar + - name: HELLO + value: world +``` + +Note the `revisionTemplate.spec.container.image` above is supplied +with the destination of the build. This enables one-step changes to +both config and source code. If the build step were responsible for +updating the `revisionTemplate.spec.container.image` at the completion +of the build, an update to both source and config could result in the +creation of two Revisions, one with the config change, and the other +with the new code deployment. It is expected that Revision will wait +for the `buildName` to be complete and the +`revisionTemplate.spec.container.image` to be live before marking the +Revision as "ready". + +Upon creating/updating the configuration's build field, the system +creates a new revision. The configuration controller will initiate a +build, populating the revision’s buildName with a reference to the +underlying Build resource. Via status updates which the revision +controller observes through the build reference, the high-level state +of the build is mirrored into conditions in the Revision’s status: + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: abc + labels: + elafros.dev/configuration: my-service + elafros.dev/configurationGeneration: 1234 + ... +spec: + # name of the build.dev/v1alpha1.Build, if built from source. + # Set by Configuration. + buildName: ... + + # spec from the configuration, with container.image containing the + # newly built container + container: # k8s core.v1.Container + image: gcr.io/... + env: + - name: FOO + value: bar + - name: HELLO + value: world +status: + # This is a copy of metadata from the container image or grafeas, indicating + # the provenance of the revision, annotated on the container + imageSource: + archive|manifest|repository: ... + context: ... + conditions: + - type: Ready + status: True + - type: BuildComplete + status: True + # other conditions indicating build failure details, if applicable +``` + +Rollout operations in the route are identical to the pre-built +container examples. + +Also analogous is updating the configuration to create a new +revision - in this case, updated source would be provided to the +configuration's inlined build spec, which would initiate a new +container build, and the creation of a new revision. + + +## 5) Deploy a Function + +**Scenario**: User deploys a new function revision to an existing service + +``` +$ elafros deploy --function index --service my-function + Deploying function to service [my-function]: +✓ Uploading [=================] +✓ Detected [node-8-9-4] runtime +✓ Building +✓ Starting +✓ Promoting + Done. + Deployed to https://my-function.default.mydomain.com +``` + +**Steps**: + +* Create/Update a Configuration, additionally specifying function details. + +**Results**: + +* The Configuration is created/updated, which generates a new revision + based on the template build and spec which can be rolled out per + previous examples + +![Build Function](images/build_function.png) + + +Previous examples illustrated creating and deploying revisions in the +context of apps. Functions are created and deployed in the same +manner (in particular, as containers which respond to HTTP). In the +build phase of the deployment, additional function metadata may be +taken into account in order to wrap the supplied code in a functions +framework. + +Functions are configured with a language-specific entryPoint. The +entryPoint may be provided as an argument to the build template, if +language-native autodetection is insufficient. By convention, a type +metadata label may also be added that designates revisions as a +function, supporting listing revisions by type; there is no change to +the system behavior based on type. + +Note that a function may be connected to one or more event sources via +Bindings in the Eventing API; the binding of events to functions is +not a core function of the compute API. + +Creating the configuration with build and function metadata: + +```http +POST /apis/elafros.dev/v1alpha1/namespaces/default/configurations +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-function +spec: + build: # build.dev/v1alpha1.BuildTemplateSpec + source: + # oneof git|gcs|custom + git: + url: https://... + commit: ... + template: # defines build template + name: go_1_9_fn # function builder + namespace: build-templates + arguments: + - name: _IMAGE + value: gcr.io/... # destination for image + - name: _ENTRY_POINT + value: index # language dependent, function-only entrypoint + + revisionTemplate: # template for building Revision + metadata: + labels: + # One-of "function" or "app", convention for CLI/UI clients to list/select + elafros.dev/type: "function" + spec: + container: # k8s core.v1.Container + image: gcr.io/... # Promise of a future build. Same as supplied in + # build.template.arguments[_IMAGE] + env: + - name: FOO + value: bar + - name: HELLO + value: world + + # serializes requests for function. Default value for functions + concurrencyModel: SingleThreaded + # max time allowed to respond to request + timeoutSeconds: 20 +``` + +Upon creating or updating the configuration, a new Revision is created +per the previous examples. Rollout operations are also identical to +the previous examples. diff --git a/spec/overview.md b/spec/overview.md new file mode 100644 index 000000000..f8bd77de8 --- /dev/null +++ b/spec/overview.md @@ -0,0 +1,86 @@ +# Resource Types + +The primary resources in the Elafros API are Routes, Revisions, and Configurations: + +* A **Route** provides a named endpoint and a mechanism for routing traffic to + +* **Revisions**, which are immutable snapshots of code + config, created by a + +* **Configuration**, which acts as a stream of environments for Revisions. + +![Object model](images/object_model.png) + +## Route + +**Route** provides a network endpoint for a user's service (which +consists of a series of software and configuration Revisions over +time). A kubernetes namespace can have multiple routes. The route +provides a long-lived, stable, named, HTTP-addressable endpoint that +is backed by one or more **Revisions**. The default configuration is +for the route to automatically route traffic to the latest revision +created by a **Configuration**. For more complex scenarios, the API +supports splitting traffic on a percentage basis, and CI tools could +maintain multiple configurations for a single route (e.g. "golden +path" and “experiments”) or reference multiple revisions directly to +pin revisions during an incremental rollout and n-way traffic +split. The route can optionally assign addressable subdomains to any +or all backing revisions. + +## Revision + +**Revision** is an immutable snapshot of code and configuration. A +revision can be created from a pre-built container image or built from +source. While there is a history of previous revisions, only those +currently referenced by a Route are addressable or routable. Older +inactive revisions need not be backed by underlying resources, they +exist only as the revision metadata in storage. Revisions are created +by updates to a **Configuration**. + +## Configuration + +A **Configuration** describes the desired latest Revision state, and +creates and tracks the status of Revisions as the desired state is +updated. A configuration might include instructions on how to transform +a source package (either git repo or archive) into a container by +referencing a [Build](https://github.com/elafros/build), or might +simply reference a container image and associated execution metadata +needed by the Revision. On updates to a Configuration, a new build +and/or deployment (creating a Revision) may be performed; the +Configuration's controller will track the status of created Revisions +and makes both the most recently created and most recently *ready* +(i.e. healthy) Revision available in the status section. + + +# Orchestration + +The system will be configured to not allow customer mutations to +Revisions. Instead, the creation of immutable Revisions through a +Configuration provides: + +* a single referenceable resource for the route to perform automated + rollouts +* a single resource that can be watched to see a history of all the + revisions created +* (but doesn’t mandate) PATCH semantics for new revisions to be done + on the server, minimizing read-modify-write implemented across + multiple clients, which could result in optimistic concurrency + errors +* the ability to rollback to a known good configuration + +In the conventional single live revision scenario, a route has a +single configuration with the same name as the route. Update +operations on the configuration enable scenarios such as: + +* *"Push code, keep config":* Specifying a new revision with updated + source, inheriting configuration such as env vars from the + configuration. +* *"Update config, keep code"*: Specifying a new revision as just a + change to configuration, such as updating an env variable, + inheriting all other configuration and source/image. + +When creating an initial route and performing the first deployment, +the two operations of creating a Route and an associated Configuration +can be done in parallel, which streamlines the use case of deploying +code initially from a button. The +[sample API usage](normative_examples.md) section illustrates +conventional usage of the API. diff --git a/spec/spec.md b/spec/spec.md new file mode 100644 index 000000000..f6d6ab825 --- /dev/null +++ b/spec/spec.md @@ -0,0 +1,263 @@ +## Resource Paths + +Resource paths in the Elafros API have the following standard k8s form: + +``` +/apis/{apiGroup}/{apiVersion}/namespaces/{metadata.namespace}/{kind}/{metadata.name} +``` + +For example: + +``` +/apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` + +It is expected that each Route will provide a name within a +cluster-wide DNS name. While no particular URL scheme is mandated +(consult the `domain` property of the Route for the authoritative +mapping), a common implementation would be to use the kubernetes +namespace mechanism to produce a URL like the following: + +``` +[$revisionname].$route.$namespace. +``` + +For example: + +``` +prod.my-service.default.mydomain.com +``` + + +# Resource YAML Definitions + +YAMLs for the Elafros API resources are described below, describing the +basic k8s structure: metadata, spec and status, along with comments on +specific fields. + +## Route + +For a high-level description of Routes, +[see the overview](overview.md#route). + +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + namespace: default + labels: + elafros.dev/type: ... # +optional convention: function|app + + # system generated meta + uid: ... +  resourceVersion: ... # used for optimistic concurrency control +  creationTimestamp: ... +  generation: ... # updated only when spec changes; used by observedGeneration +  selfLink: ... + ... +spec: + traffic: + # list of oneof configurationName | revisionName. + # configurationName watches configurations to address latest latestReadyRevisionName + # revisionName pins a specific revision + - configurationName: ... + name: ... # +optional. Access as {name}.${status.domain}, + # e.g. oss: current.my-service.default.mydomain.com + percent: 100 # list percentages must add to 100. 0 is a valid list value + - ... + +status: + # domain: The hostname used to access the default (traffic-split) + # route. Typically, this will be composed of the name and namespace + # along with a cluster-specific prefix (here, mydomain.com). + domain: my-service.default.mydomain.com + + traffic: + # current rollout status list. configurationName references + # are dereferenced to latest revision + - revisionName: ... # latestReadyRevisionName from a configurationName in spec + name: ... + percent: ... # percentages add to 100. 0 is a valid list value + - ... + + conditions: # See also the [error conditions documentation](errors.md) + - type: RolloutComplete + status: True + - type: TrafficDropped + status: False + - ... + + observedGeneration: ... # last generation being reconciled +``` + + +## Configuration + +For a high-level description of Configurations, +[see the overview](overview.md#configuration). + + +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service + namespace: default + + # system generated meta + uid: ... +  resourceVersion: ... # used for optimistic concurrency control +  creationTimestamp: ... +  generation: ... # updated only when spec changes; used by observedGeneration +  selfLink: ... + ... +spec: + # +optional. composable Build spec, if omitted provide image directly + build: # This is a build.dev/v1alpha1.BuildTemplateSpec + source: + # oneof git|gcs|custom: + + # +optional. + git: + url: https://github.com/jrandom/myrepo + commit: deadbeef # Or branch, tag, ref + + # +optional. A zip archive or a manifest file in Google Cloud + # Storage. A manifest file is a file containing a list of file + # paths, backing URLs, and sha checksums. Manifest may be a more + # efficient mechanism for a client to perform partial upload. + gcs: + location: https://... + type: 'archive' # Or 'manifest' + + # +optional. Custom specifies a container which will be run as + # the first build step to fetch the source. + custom: # is a core.v1.Container + image: gcr.io/cloud-builders/git:latest + args: [ "clone", "https://...", "other-place" ] + + template: # build template reference and arguments. + name: go_1_9_fn # builder name. Functions may have custom builders + namespace: build-templates + arguments: + - name: _IMAGE + value: gcr.io/... # destination for image + - name: _ENTRY_POINT + value: index # if function, language dependent entrypoint + + revisionTemplate: # template for building Revision + metadata: ... + labels: + elafros.dev/type: "function" # One of "function" or "app" + spec: # elafros.RevisionTemplateSpec. Copied to a new revision + + # +optional. if rolling back, the client may set this to the + # previous revision's build to avoid triggering a rebuild + buildName: ... + + # is a core.v1.Container; some fields not allowed, such as resources, ports + container: + # image either provided as pre-built container, or built by Elafros from + # source. When built by elafros, set to the same as build template, e.g. + # build.template.arguments[_IMAGE], as the "promise" of a future build. + # If buildName is provided, it is expected that this image will be + # present when the referenced build is complete. + image: gcr.io/... + command: ['run'] + args: [] + env: + # list of environment vars + - name: FOO + value: bar + - name: HELLO + value: world + - ... + livenessProbe: ... # Optional + readinessProbe: ... # Optional + + # +optional concurrency strategy. SingleThreaded default value for functions + concurrencyModel: SingleThreaded + # +optional. max time the instance is allowed for responding to a request + timeoutSeconds: ... + serviceAccountName: ... # Name of the service account the code should run as. + +status: + # the latest created and ready to serve. Watched by route + latestReadyRevisionName: abc + # latest created revision, may still be in the process of being materialized + latestCreatedRevisionName: def + conditions: # See also the [error conditions documentation](errors.md) + - type: LatestRevisionReady + status: False + reason: ContainerMissing + message: "Unable to start because container is missing and build failed." + observedGeneration: ... # last generation being reconciled +``` + + +## Revision + +For a high-level description of Revisions, +[see the overview](overview.md#revision). + +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: myservice-a1e34 # system generated + namespace: default + labels: + elafros.dev/configuration: ... # to list configurations/revisions by service + elafros.dev/configurationGeneration: ... # generation of configuration that created this Revision + elafros.dev/type: "function" # convention, one of "function" or "app" + # system generated meta + uid: ... +  resourceVersion: ... # used for optimistic concurrency control +  creationTimestamp: ... +  generation: ... +  selfLink: ... + ... + +# spec populated by Configuration +spec: + # +optional. name of the build.dev/v1alpha1.Build if built from source + buildName: ... + + container: # core.v1.Container + image: gcr.io/... + command: ['run'] + args: [] + env: # list of environment vars + - name: FOO + value: bar + - name: HELLO + value: world + - ... + livenessProbe: ... # Optional + readinessProbe: ... # Optional + concurrencyModel: ... + timeoutSeconds: ... + serviceAccountName: ... # Name of the service account the code should run as. + ... +status: + # This is a copy of metadata from the container image or grafeas, + # indicating the provenance of the revision. This is based on the + # container image, but may need further clarification. + imageSource: + git|gcs: ... + conditions: # See also the documentation in errors.md + - type: Ready + status: False + message: "Starting Instances" + # if built from source: + - type: BuildComplete + status: True + # other conditions indicating build failure, if applicable + - ... + # URL for accessing the logs generated by this revision. Note that logs + # may still be access controlled separately from access to the API object. + logUrl: "logging.infra.mycompany.com/...?filter=revision=myservice-a1e34&..." +``` + +