From a4d13ed26c0aed6be4fbfdd7f079d6754003d94f Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Mon, 10 Mar 2025 19:05:43 +0100 Subject: [PATCH 1/2] Init app abstraction Signed-off-by: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> --- app-abstraction/README.md | 103 ++++++++++++++++++++++ app-abstraction/app.cue | 36 ++++++++ app-abstraction/cue.mod/module.cue | 4 + app-abstraction/platform/main.cue | 129 ++++++++++++++++++++++++++++ app-abstraction/platform/policy.cue | 7 ++ app-abstraction/render | 3 + 6 files changed, 282 insertions(+) create mode 100644 app-abstraction/README.md create mode 100644 app-abstraction/app.cue create mode 100644 app-abstraction/cue.mod/module.cue create mode 100644 app-abstraction/platform/main.cue create mode 100644 app-abstraction/platform/policy.cue create mode 100755 app-abstraction/render diff --git a/app-abstraction/README.md b/app-abstraction/README.md new file mode 100644 index 0000000..dae4343 --- /dev/null +++ b/app-abstraction/README.md @@ -0,0 +1,103 @@ +# Application Abstraction + +This pattern shows how you can define an application across several deployment targets (such as development and production environments). + +## Concept + +The application abstraction uses a unified configuration approach whereby the configuration for all possible states are represented within a shared context. Denoting differing configuration states as *environments*, this has several benefits: +- A declarative approach to defining environment-specific configuration +- Enhanced readability of configuration by sticking to CUE's core language design principles +- The ability to define cross-environment values, e.g. "This value in preprod should be the same in prod" +- Allows users to compose their abstracted configuration however they want + +## Structure + +The platform abstractions are defined in [platform](./platform/) under a seperate package. This package can be published within a module and distributed internally within your organisation. + +The abstraction user defines their app as shown in [app.cue](./app.cue), with the [render](./render) script showing how you might choose to retrieve the rendered output from the configuration. + +### Platform Package + +The `platform` package contains two key definitions; `#Deployment`, and `#App`. `#Deployment` defines which deployment targets needs to be defined and how. It also puts these together into a lookup of deployment target to manifest list. `#App` is a union of valid abstractions (in this case, only one called `#WebApp`). A user can choose this abstraction to populate their "desired" state for each deployment target. + +## Usage + +For example, to retrieve the YAML manifest stream for the preview environment: +```yaml +$ ./render preview +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: test-app +spec: + hostnames: + - test-app.example-dev.com + parentRefs: + - name: web-gateway-lb + rules: + - backendRefs: + - name: test-app + port: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: test-app +spec: + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + selector: + app.kubernetes.io/name: test-app +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app + labels: + app.kubernetes.io/name: test-app +spec: + selector: + matchLabels: + app.kubernetes.io/name: test-app + template: + metadata: + labels: + app.kubernetes.io/name: test-app + spec: + containers: + - name: test-app + image: my-registry.com/test-app:v3 + ports: + - containerPort: 8080 + env: + - name: DEBUG + value: "true" + resources: + requests: + cpu: 1 + memory: 2Gi + limits: + memory: 4Gi +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: test-app +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: test-app + minReplicas: 1 + maxReplicas: 1 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 0.7 + +``` diff --git a/app-abstraction/app.cue b/app-abstraction/app.cue new file mode 100644 index 0000000..12781b3 --- /dev/null +++ b/app-abstraction/app.cue @@ -0,0 +1,36 @@ +package app + +import "github.com/cue-lang/cue-deployment-patterns/app-abstraction/platform" + +platform.#Deployment & { + let base = platform.#WebApp & { + AppName=name: "test-app" + image: { + registry: "my-registry.com" + name: AppName + } + AppPort=port: 8080 + healthCheck: port: AppPort + scaling: cpuUtilizationThreshold: 0.7 + } + + preview: base & { + image: tag: "v3" + hostname: "test-app.example-dev.com" + env: DEBUG: "true" + scaling: minReplicas: 1 + } + preprod: base & { + image: tag: "v2" + hostname: "test-app.example-preprod.com" + scaling: prod.scaling + } + prod: base & { + image: tag: "v1" + hostname: "test-app.example.com" + scaling: { + minReplicas: 3 + maxReplicas: minReplicas * 10 + } + } +} diff --git a/app-abstraction/cue.mod/module.cue b/app-abstraction/cue.mod/module.cue new file mode 100644 index 0000000..c3aaf82 --- /dev/null +++ b/app-abstraction/cue.mod/module.cue @@ -0,0 +1,4 @@ +module: "github.com/cue-lang/cue-deployment-patterns/app-abstraction" +language: { + version: "v0.12.0" +} diff --git a/app-abstraction/platform/main.cue b/app-abstraction/platform/main.cue new file mode 100644 index 0000000..58ec136 --- /dev/null +++ b/app-abstraction/platform/main.cue @@ -0,0 +1,129 @@ +package platform + +#ConstrainedString: string & =~"^[A-Za-z-]{1,253}$" +#Port: int & >=1024 & <=65535 + +#Deployment: { + preview: #App + preprod: #App + prod: #App + + let deploymentTargets = { + "preview": preview + "preprod": preprod + "prod": prod + } + + for deploymentTargetName, appDefinition in deploymentTargets { + let httpRoute = { + apiVersion: "gateway.networking.k8s.io/v1" + kind: "HTTPRoute" + metadata: name: appDefinition.name + spec: { + hostnames: [appDefinition.hostname] + parentRefs: [{name: "web-gateway-lb"}] + rules: [{ + backendRefs: [{ + name: service.metadata.name + port: service.spec.ports[0].port + }] + }] + } + } + + let service = { + apiVersion: "v1" + kind: "Service" + metadata: name: appDefinition.name + spec: { + ports: [{ + protocol: "TCP" + port: appDefinition.port + targetPort: port + }] + selector: deployment.metadata.labels + } + } + + let deployment = { + apiVersion: "apps/v1" + kind: "Deployment" + DeploymentMetadata=metadata: { + name: appDefinition.name + labels: "app.kubernetes.io/name": appDefinition.name + } + spec: { + selector: matchLabels: DeploymentMetadata.labels + template: { + metadata: labels: DeploymentMetadata.labels + spec: containers: [{ + name: DeploymentMetadata.name + let imageRef = appDefinition.image + image: "\(imageRef.registry)/\(imageRef.name):\(imageRef.tag)" + ports: [{containerPort: appDefinition.port}] + env: [for k, v in appDefinition.env {name: k, value: v}] + resources: { + requests: { + cpu: 1 + memory: "2Gi" + } + limits: { + memory: "4Gi" + } + } + }] + } + } + } + + let autoscaler = { + apiVersion: "autoscaling/v2" + kind: "HorizontalPodAutoscaler" + metadata: name: appDefinition.name + spec: { + scaleTargetRef: { + apiVersion: deployment.apiVersion + kind: deployment.kind + name: deployment.metadata.name + } + minReplicas: appDefinition.scaling.minReplicas + maxReplicas: appDefinition.scaling.maxReplicas + metrics: [{ + type: "Resource" + resource: { + name: "cpu" + target: { + type: "Utilization" + averageUtilization: appDefinition.scaling.cpuUtilizationThreshold + } + } + }] + } + } + + manifests: (deploymentTargetName): [httpRoute, service, deployment, autoscaler] + } +} + +#App: #WebApp // | #OtherApp + +#WebApp: { + name: #ConstrainedString + image: { + registry: string + name: #ConstrainedString + tag: string + } + port: #Port + env: [EnvName=string]: string + hostname: string + healthCheck: { + path: string | *"/healthz" + port: #Port + } + scaling: { + minReplicas: int & >0 //& <=maxReplicas + maxReplicas: int & <=999 | *minReplicas //& >=minReplicas + cpuUtilizationThreshold: float & >=0.1 & <=0.95 + } +} diff --git a/app-abstraction/platform/policy.cue b/app-abstraction/platform/policy.cue new file mode 100644 index 0000000..c5cfc7d --- /dev/null +++ b/app-abstraction/platform/policy.cue @@ -0,0 +1,7 @@ +package platform + +#Deployment: { + preview: hostname: =~ "example-dev.com$" + preprod: hostname: =~ "example-preprod.com$" + prod: hostname: =~ "example.com$" +} diff --git a/app-abstraction/render b/app-abstraction/render new file mode 100755 index 0000000..bd9cd19 --- /dev/null +++ b/app-abstraction/render @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +CUE_EXPERIMENT=evalv3 cue export -e "yaml.MarshalStream(manifests.$1)" --out text From 9d11ca53cd47b9b1b1bae869d2452ad9c8cbabaf Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Mon, 10 Mar 2025 19:15:30 +0100 Subject: [PATCH 2/2] Update README to include user requirements for the pattern Signed-off-by: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> --- app-abstraction/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app-abstraction/README.md b/app-abstraction/README.md index dae4343..896d2a4 100644 --- a/app-abstraction/README.md +++ b/app-abstraction/README.md @@ -10,6 +10,13 @@ The application abstraction uses a unified configuration approach whereby the co - The ability to define cross-environment values, e.g. "This value in preprod should be the same in prod" - Allows users to compose their abstracted configuration however they want +## User Requirements + +- Hide implementation details from users +- Enforce platform policies on users +- Support several deployment targets such as pre-prod and prod environments +- Construct complex configuration with a simplified interface + ## Structure The platform abstractions are defined in [platform](./platform/) under a seperate package. This package can be published within a module and distributed internally within your organisation.