Skip to content

App Abstraction Pattern #2

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
110 changes: 110 additions & 0 deletions app-abstraction/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# 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

## 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.

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

```
36 changes: 36 additions & 0 deletions app-abstraction/app.cue
Original file line number Diff line number Diff line change
@@ -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
}
}
}
4 changes: 4 additions & 0 deletions app-abstraction/cue.mod/module.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module: "github.com/cue-lang/cue-deployment-patterns/app-abstraction"
language: {
version: "v0.12.0"
}
129 changes: 129 additions & 0 deletions app-abstraction/platform/main.cue
Original file line number Diff line number Diff line change
@@ -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
}
}
7 changes: 7 additions & 0 deletions app-abstraction/platform/policy.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package platform

#Deployment: {
preview: hostname: =~ "example-dev.com$"
preprod: hostname: =~ "example-preprod.com$"
prod: hostname: =~ "example.com$"
}
3 changes: 3 additions & 0 deletions app-abstraction/render
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

CUE_EXPERIMENT=evalv3 cue export -e "yaml.MarshalStream(manifests.$1)" --out text