Не так давно Apple выпустили раннюю версию своего нового языка программирования специального назначения – Pkl, предназначенного для написания конфигураций. Несмотря на то, что этот язык, по сути, только родился, он уже с пособен на некоторые интересные вещи. В этой статье опробуем Pkl путём написания YAML манифестов для Kubernetes.
Что такое Pkl?
Pkl (читается, как Pickle) – это, как я уже выше написал, ЯП специального назначения для создания конфигураций. Синтаксис напоминает HCL и Nginx, только с кучей вспомогательных функций.
Pkl поддерживает валидацию полей через проставленые типов, проверки
типа if
, циклы, функции, наследование и GET http запросы.
Всё это в настолько примитивном виде, на сколько это возможно.
Перевести код на Pkl вы можете в JSON, YAML, XML и другие форматы. Он также может генерировать код на Go, Java, Kotlin и Swift, но в статьей это рассматривать не будем.
Пример
name = "Swallow"
job {
title = "Sr. Nest Maker"
company = "Nests R Us"
yearsOfExperience = 2
}
{
"name": "Swallow",
"job": {
"title": "Sr. Nest Maker",
"company": "Nests R Us",
"yearsOfExperience": 2
}
}
Манифесты Kubernetes на Pkl
Для примера я взял манифесты своего pet-проекта, которые
включают в себя Deployment
, Service
и Ingress
ресурсы, и всё переписал на Pkl.
Deployment манифест
apiVersion: apps/v1
kind: Deployment
metadata:
name: davy-page-blog
namespace: davy
spec:
selector:
matchLabels:
app: davy-page-blog
template:
metadata:
labels:
app: davy-page-blog
spec:
containers:
- name: davy-page
image: image:v0.0.81
imagePullPolicy: IfNotPresent
resources:
...
readinessProbe:
...
livenessProbe:
...
imagePullSecrets:
- name: vault-registry-secret
Service манифест
apiVersion: v1
kind: Service
metadata:
name: davy-page-blog
namespace: davy
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
name: http
selector:
app: davy-page-blog
Ingress манифест
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: davy-page-blog
namespace: davy
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- davy.page
secretName: davy-tls
rules:
- host: davy.page
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: davy-page-blog
port:
number: 80
Основная задача заключалась в том, чтобы взять за основу текущие манифесты, которые
у меня использовались для поднятия одного стенда, и создать конфигурацию на Pkl, с помощью которой,
я смогу легко генерировать манифесты для трех разных окружений: prod-type-1
, prod-type-2
и beta-(0..999)
.
Для валидации конфигов уже есть готовый модуль pkl-k8s. Он содержит в себе типизированные объекты для ресурсов Kubernetes. Если вы будете добавлять в будущий k8s ресурс поля, которых там не должно быть, или будете корректным полям выставлять некорректные значения, то при сборке конфига получите ошибку.
По итогу у меня получился вот такой главный файл, который создаёт необходимые манифесты для стендов любого типа:
import "@k8s/K8sResource.pkl"
import "./DavyPageStand.pkl"
stand = new DavyPageStand.Stand {
args = new {
// Все аргументы, которые нужны берутся из ENV переменных.
// Если их вдруг нет, то выставится либо дефолтное значение, либо null.
type = read?("env:TYPE") ?? "blog"
version = read?("env:VERSION") ?? "v0.0.81"
id = read?("env:ID")
}
}
output {
value = new Listing<K8sResource> {
stand.deployment
stand.service
stand.ingress
}
renderer = (K8sResource.output.renderer as YamlRenderer) {
isStream = true
}
}
Для гибкости, если вдруг нужно подредактировать, в полученных объектах, конкретные поля, можно сделать так (в моём понимаение это костыль, и так смысл есть делать, только если нужно прям супер срочно что-то починить и нет желания смотреть другие Pkl файлы):
...
value = new Listing<K8sResource> {
(stand.deployment) {
metadata {
name = "test"
}
spec {
template {
spec {
containers {
// меняем образ только у контейнера с таким именем
[[name == "davy-page"]] {
image = "bruh"
}
}
}
}
}
}
stand.service
stand.ingress
}
...
Конструкция (object) { ... }
обнозначает наследовани е. Так можно накладывать на какой-то
существующий объект свои новые значения.
Этот главный файл ссылается на DavyPageStand.pkl
, который объединяет
файлы: DavyPageDeployment.pkl
, DavyPageService.pkl
, DavyPageIngress.pkl
, каждый из которых
в свою очередь отвечает за создание объектов для своего ресурса (для какого можно понять по названию).
DavyPageStand.pkl
import "@k8s/K8sResource.pkl"
import "./DavyPageDeployment.pkl"
import "./DavyPageService.pkl"
import "./DavyPageIngress.pkl"
const function getHost(type, id) = if (id != null) "pr-\(id).\(type).dev.davy.page" else if (type == "blog") "davy.page" else "p.davy.page"
class Args {
type: String
version: String
id: String?
}
class Stand {
args: Args
_args = args // Костыль (про него будет ниже в статье)
ammend_deploy: Mapping
type_name = if (args.type == "blog") "davy-page-blog" else "davy-page-private"
name = if (args.id != null) "\(type_name)-\(args.id)" else "\(type_name)"
variant = if (args.id != null) "dev" else "prod"
deployment = new DavyPageDeployment.DPL {
args = new {
resource_name = name
version = _args.version
labels {
["app-variant"] = variant
}
}
...ammend_deploy
}
service = new DavyPageService.SVC {
args = new {
resource_name = name
labels {
["app-variant"] = variant
}
}
}
ingress = new DavyPageIngress.ING {
args = new {
resource_name = name
resource_host = getHost(_args.type, _args.id)
with_auth = _args.id != null || _args.type == "private"
labels {
["app-variant"] = variant
}
}
}
}
DavyPageDeployment.pkl
import "@k8s/api/apps/v1/Deployment.pkl"
import "@k8s/api/core/v1/Probe.pkl"
import "@k8s/api/core/v1/ResourceRequirements.pkl"
const defaultProbe = new Probe {
httpGet {
scheme = "HTTP"
path = "/img/logo.svg"
port = 80
}
failureThreshold = 3
timeoutSeconds = 1
}
const params {
readinessProbe = (defaultProbe) {
initialDelaySeconds = 10
periodSeconds = 5
}
livenessProbe = (defaultProbe) {
initialDelaySeconds = 15
periodSeconds = 10
}
resources = new ResourceRequirements {
requests {
["memory"] = "170Mi"
["cpu"] = "10m"
}
limits {
["memory"] = "340Mi"
["cpu"] = "100m"
}
}
image = "my-image"
image_pull_secret = "vault-registry-secret"
}
class Args {
resource_name: String
version: String
labels: Mapping<String, String>?
}
class DPL extends Deployment {
hidden args: Args
metadata {
namespace = "davy"
name = args.resource_name
labels {
["app"] = "\(args.resource_name)"
...args.labels
}
}
spec {
selector {
matchLabels {
["app"] = args.resource_name
}
}
template {
metadata {
labels {
["app"] = args.resource_name
}
}
spec {
containers {
new {
name = "davy-page"
image = "\(params.image):\(args.version)"
imagePullPolicy = "IfNotPresent"
resources = params.resources
readinessProbe = params.readinessProbe
livenessProbe = params.livenessProbe
}
}
imagePullSecrets {
new {
name = params.image_pull_secret
}
}
}
}
}
}
DavyPageService.pkl
import "@k8s/api/core/v1/Service.pkl"
class Args {
resource_name: String
labels: Mapping<String, String>
}
class SVC extends Service {
hidden args = Args
metadata {
namespace = "davy"
name = args.resource_name
labels {
["app"] = "\(args.resource_name)"
...args.labels
}
}
spec {
type = "ClusterIP"
ports {
new {
port = 80
targetPort = 80
name = "http"
}
}
selector {
["app"] = args.resource_name
}
}
}
DavyPageIngress.pkl
import "@k8s/api/networking/v1/Ingress.pkl"
class Args {
resource_name: String
resource_host: String
with_auth: Boolean
labels: Mapping<String, String>?
}
class ING extends Ingress {
hidden args: Args
metadata {
namespace = "davy"
name = args.resource_name
annotations {
["cert-manager.io/cluster-issuer"] = "letsencrypt-prod"
when (args.with_auth) {
["nginx.ingress.kubernetes.io/auth-type"] = "basic"
["nginx.ingress.kubernetes.io/auth-secret"] = "private-basic-auth"
["nginx.ingress.kubernetes.io/auth-realm"] = "private zone"
}
}
labels {
["app"] = "\(args.resource_name)"
...args.labels
}
}
spec {
ingressClassName = "nginx"
tls {
new {
hosts { args.resource_host }
secretName = "\(args.resource_name)-tls"
}
}
rules {
new {
host = args.resource_host
http {
paths {
new {
path = "/"
pathType = "Prefix"
backend {
service {
name = args.resource_name
port {
name = "http"
}
}
}
}
}
}
}
}
}
}
На выходе я получаю рабочий k8s манифест состоящий из трёх ресурсов:
Пример результата в YAML формате
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: davy-page-blog
app-variant: prod
name: davy-page-blog
namespace: davy
spec:
template:
metadata:
labels:
app: davy-page-blog
spec:
imagePullSecrets:
- name: vault-registry-secret
containers:
- image: my-image:v0.0.81
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 3
periodSeconds: 10
timeoutSeconds: 1
initialDelaySeconds: 15
httpGet:
path: /img/logo.svg
scheme: HTTP
port: 80
resources:
requests:
memory: 170Mi
cpu: 10m
limits:
memory: 340Mi
cpu: 100m
name: davy-page
readinessProbe:
failureThreshold: 3
periodSeconds: 5
timeoutSeconds: 1
initialDelaySeconds: 10
httpGet:
path: /img/logo.svg
scheme: HTTP
port: 80
selector:
matchLabels:
app: davy-page-blog
---
apiVersion: v1
kind: Service
metadata:
labels:
app: davy-page-blog
app-variant: prod
name: davy-page-blog
namespace: davy
spec:
ports:
- port: 80
name: http
targetPort: 80
type: ClusterIP
selector:
app: davy-page-blog
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
labels:
app: davy-page-blog
app-variant: prod
name: davy-page-blog
namespace: davy
spec:
ingressClassName: nginx
rules:
- host: davy.page
http:
paths:
- path: /
backend:
service:
port:
name: http
name: davy-page-blog
pathType: Prefix
tls:
- secretName: davy-page-blog-tls
hosts:
- davy.page
Проблемы
Есть несколько проблем, с которыми лично я столкнулся, во время работы с Pkl.
1. Указание ключей объектам
Начну с самого бесячего. Если вы хотите полю в объекте назначить переменную с таким же названием – вы получите Stack Overflow. Потому что Pkl будет подставлять вашу переменную вместо и ключа, и значения.
arg = "test"
object = new MyObject {
arg = arg
}
Чтобы этого избежать, приходится придумывать костыли:
_arg = "test"
object = new MyObject {
arg = _arg
}