-
Notifications
You must be signed in to change notification settings - Fork 3
Lifecycle
Hub CLI performs lifecycle operations such as deploy
, undeploy
, backup
on stack components by calling implementation provided by the component in a sequence defined in stack manifest. Stack deployment starts with parameters supplied by user. Each component declares required parameters and may produce some outputs. In turn, outputs become parameters for subsequent components.
We highly recommend for both deploy and undeploy to be idempotent operation.
Execution of lifecycle operation requires elaborate-d manifest hub.yaml.elaborate
which is an assembly of hub.yaml
stack and hub-component.yaml
components manifests with some transformation applied to parameters, usually declared in params*.yaml
. See Manifest and Manifest reference for details.
hub elaborate hub.yaml params.yaml -o hub.yaml.elaborate
After hub.yaml.elaborate
is created:
hub deploy hub.yaml.elaborate -e NAME=stage
Components will be deployed in sequence: templates are formatted, OS environment variables are exported, component implementation is called, outputs are parsed and stored into state, state file is persisted; then next component is processed until order
is exhausted. On error Hub CLI will stop, until forced with -f / --force
. With -d / --debug
it will additionally show parameters values and outputs captured.
Implementation search precedence:
- Makefile with
deploy:
target - One of
deploy
,bin/deploy
,_deploy
,deploy.sh
bin/deploy.sh
_deploy.sh
- Helm is triggered by presence of any of
values.yaml
,values.yaml.template
,values.yaml.gotemplate
- Kustomize by
kustomization.yaml
,kustomization.yaml.template
,kustomization.yaml.gotemplate
- Skaffold by
skaffold.yaml
,skaffold.yaml.template
,skaffold.yaml.gotemplate
- Terraform by
*.tf
or*.tf.*
Implementations for 2-6 are provided by Hub CLI Deployment extensions.
Outputs capture are historically tailored to Terraform format, but anything including echo
will do:
Outputs:
vpc = vpc-123456
role_arn = arn:aws:iam:...
secrets = <one-time pad gibberish>
<empty line>
Multiple Output:
blocks are supported.
Before component implementation is invoked, Hub CLI verifies that each requirement declared in requires
is either provided by external means - by the environment or platform stack, or is short-circuited to in-stack provides
of component prior in the sequence.
Kubernetes requirement has as special meaning to Hub CLI. When requires: kubernetes
is encountered, Hub CLI will check if ${dns.domain}
Kubeconfig context exists. If not, then Hub CLI will create one by using dns.domain
, kubernetes.api.*
stack parameters or component outputs.
Hub CLI parameter hub.provides
and HUB_PROVIDES
env var exports a space-separated list of currently known provides to the component.
Component may declare provides
in hub-component.yaml
. Additionally, dynamic provides could be sent as outputs:
Outputs:
provides = tls-ingress daemonset-ingress
<empty line>
If lifecycle.bare: true
is set then missing deploy
/ undeploy
implementation is not an error. This is to produce synthetic outputs.
Component state deployed
or error
is written into state file. If component error-ed then log is also saved. State file can be examined by `hub explain [--op-log].
hub invoke
could be used to call component verbs other than deploy
, undeploy
, and backup
. For example:
hub invoke vault unseal
hub invoke vault connect
The implementation for unseal
will be searched and invoked; templates are processed and OS environment variables are set according to component manifest.
When component declares requires
- a list of capabilities that must be provided by prior components, platform stack, or environment - it became a list of hard requirements that must be satisfied before component (deploy) implementation is invoked. Failure to satisfy any requirement is hard error.
Component provides capabilities by declaring them in component's manifest provides
list. Additionally, dynamic capabilities are collected from deploy
output Provides:
block, similar to Outputs:
.
An aggregation of component's provides became (platform) stack provides.
A list of well known requires
are built into Hub CLI. Such requirements could be sourced from environment, such as aws
(a working AWS CLI). These requirements cannot be tuned.
A component might be declared optional on stack level
lifecycle:
optional:
- s3-bucket
Failure to deploy optional component is soft error. By default, all components are mandatory. Those declared optional
became optional. If mandatory
components are specified, then everything else is optional.
lifecycle:
mandatory:
- kubernetes
- tiller
- traefik
A requirement could be softened by tuning it via lifecycle
in stack manifest:
lifecycle:
requires:
optional:
- vault
- cdn:control-plane
- component.ingress.ssl.enabled:acm
vault
and cdn
are capabilities, control-plane
and acm
are component names. vault
requirement is relaxed for all components, while cdn
just for control-plane
. Requirement became optional. acm
is deployed only if ingress parameter component.ingress.ssl.enabled
requests TLS-enabled stack (evaluates to truth-y value, ie. everything else besides false
, ""
empty string, 0
, (unknown)
).
When an optional requirement cannot be satisfied for a particular component, the component deployment (or undeployment) implementation is not invoked.
Requirements tuning of Base stack will be merged with tuning of derived stack. The following rules are used:
-
requirement
supersedes all priorrequirement:component
- making requirement optional for everyone; -
requirement:component
supersedes priorrequirement
- making requirement optional forcomponent
. There could be multiple declarations for different components; -
:
erase all prior tuning; -
requirement:
erase all prior tuning forrequirement
; -
:component
erase all prior tuning forcomponent
; - otherwise, the entry is appended.
Undeploy is very similar to deploy, except outputs are not captured and only component status - undeployed
or error
+log is written into state.
Components that export backup
verb are subject for hub backup
processing. See Backup.
Below is detailed explanation how parameters and outputs are processed within Hub CLI.
During elaborate
Hub CLI creates a final manifest assembly that consists of:
- a leading document constructed from
hub.yaml
,params*.yaml
,fromStack
YAML-s, adding parameters markedkind: user
in*/hub-component.yaml
; - all component's
hub-component.yaml
with the component name (meta.name
) changed to matchhub.yaml
specified name.
Let's take Agile Stack's infra Dev stack as an example. The stack is built from:
-
agilestacks/hub.yaml
- core infra; -
agilestacks/params.yaml
- parameters for the infra; -
dev/hub.yaml
- usesfromStack: agilestacks
and adds some components: prometheus, etc.; -
dev/params.yaml
- parameters for additional components; -
dev/params-dev.yaml
- additional Dev stack parameters.
Separation between hub.yaml
and params*.yaml
is a convenience.
The files will be parsed and parameters will be arranged by hub elaborate
in following order:
-
hub-component.yaml
of allagilestacks
components, in order ofcomponents:
-
hub-component.yaml
of alldev
components agilestacks/hub.yaml
agilestacks/params.yaml
dev/hub.yaml
dev/params.yaml
dev/params-dev.yaml
hub-component.yaml
kind: user
parameters (1) are brought to the top level - first document of hub.yaml.elaborate
with a component
qualifier.
Parameters from (1), (2) to (7) are merged in the order specified:
-
kind: user
takes priority overkind: tech
. -
value
,default
,fromEnv
,env
, andbrief
are overwritten. - In case top-level parameter - ie. from
hub.yaml
orparams*.yaml
has nocomponent
qualifier, thenelaborate
additionally checks for all previously encountered parameters of the same name with a qualifier, then do merge into those. This is the reason for the following inhub.yaml.elaborate
:
- name: component.auth-service.authApiSecret
kind: user
fromEnv: AUTH_API_SECRET
- name: component.auth-service.authApiSecret
component: auth-service
kind: user
fromEnv: AUTH_API_SECRET
During deploy
following things happens:
- Top-level parameters are locked:
-
kind: user
parameters with no values are fetched fromfromEnv
(if any) or asked on the terminal (if stdin is a TTY); -
default
values are substituted; - expression evaluations are performed;
- cycles become errors;
- empty parameters become errors until suppressed with
empty: allow
; - all errors and warnings are collected and reported.
- If there is a state file, then parameters from state are loaded and merged into global state - but only empty (stack) values are replaced (by state values) and warnings are emitted.
- The locked set of top-level parameters are never changed after this step.
Top-level parameters can reference each other via expressions.
On each component deploy:
- Parameters are expanded:
- parameters with no values are searched in the top-level scope taking in account
component
anddepends
qualifiers; - expression evaluations are performed;
- unknown empty parameters (without
value:
expression) become errors until suppressed withempty: allow
at which point their value assigned to empty string.
- Component parameters are never sent to top-level scope, but they are written into state file for informational purposes.
-
env:
variables are set, then merged on top of OS environ to create process environment for component invocation. - Outputs are collected and put onto global outputs scope - with a qualifier - of the component producing the output
${component:parameter}
. So that when parameters qualifiers are processed, thename|qualifier
is searched first and then just plainname
. Outputs and parameters are processed independently although they looks very similar to the user. Outputs have higher precedence.
Component-level parameters may reference each other in expressions, top to bottom.
Set parameter value: " "
or empty: allow
to make empty parameter on hub-component.yaml
level. During template processing and OS environment setup spaces are trimmed, effectively creating an empty substitution.
Capturing empty parameter from top-level scope does not require empty: allow
on component level. But it might be useful to proceed with deployment when optional upstream component failed thus no outputs were produced.
In step (2) above, empty stack parameters are replaced with values from state, if any. This is done for two reasons:
- In a collaborative environment, my peers may have already introduced a change into stack state - added a new stack-level parameter, for example, but this change is either (a) not yet completelly communicated via version control (Git), or (b) I have no known value to set
fromEnv
variable to. Yet I'd like to make changes (deploy or undeploy a component) that are not related to the change introduced. Thus loading a missing value to proceed is useful in such workflow. - Due to technical errors on my workstation, such as incomplete environment setup, out-of-date source code, etc., I may perform a destructive operation without any indication - by re-deploying a component with optional parameter unset or defaulted to incorrect value.
In case the previously set value must be erased, Hub CLI requires this to be specified explicitly by setting stack-level parameter to <space>
:
- name: component.dex.passwordDb.email
value: ' '
This is consistent with apply semantic in Terraform and kubectl for scalar attributes.
© 2022 EPAM Systems, Inc. All Rights Reserved