Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Makefile.vars.mk
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ KUBENT_IMAGE ?= ghcr.io/doitintl/kube-no-trouble:latest
KUBENT_DOCKER ?= $(DOCKER_CMD) $(DOCKER_ARGS) $(root_volume) --entrypoint=/app/kubent $(KUBENT_IMAGE)

instance ?= defaults
test_instances = tests/defaults.yml
test_instances = tests/defaults.yml tests/resources.yml
64 changes: 63 additions & 1 deletion class/defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,66 @@ parameters:
talos_backup:
=_metadata:
multi_tenant: true
namespace: syn-talos-backup

namespace:
name: syn-talos-backup
labels: {}
annotations: {}

images:
talos_backup:
registry: ghcr.io
repository: siderolabs/talos-backup
# Pinned to a git-describe tag of main HEAD until upstream cuts a
# stable release. Tagged v0.1.0-beta.* releases lack zstd,
# path-style, and multi-recipient age. Bump manually after watching
# https://github.com/siderolabs/talos-backup/releases — Renovate is
# disabled for this image (see renovate.json).
tag: v0.1.0-beta.3-10-gb9fd478
pull_policy: IfNotPresent

schedule: "0 */6 * * *"
successful_jobs_history_limit: 3
failed_jobs_history_limit: 1
concurrency_policy: Forbid

# Talos API ServiceAccount (talos.dev/v1alpha1) granting os:etcd:backup.
# Requires kubernetesTalosAPIAccess machineconfig allow-listing the
# deploy namespace and the os:etcd:backup role.
talos_service_account:
name: talos-backup-secrets

s3:
bucket: ""
region: us-east-1
endpoint: ""
use_path_style: false
prefix: ""
credentials:
create: false
name: talos-backup-s3
access_key_id: ""
secret_access_key: ""

# Optional. Falls back to the talosconfig context name when empty.
cluster_name: ""

# Required. List of age public keys to encrypt the snapshot for.
age_recipient_public_keys: []

enable_compression: false

# Extra env vars appended to the container.
extra_env: {}

resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi

node_selector: {}
tolerations: []
affinity: {}
10 changes: 9 additions & 1 deletion component/app.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ local inv = kap.inventory();
local params = inv.parameters.talos_backup;
local argocd = import 'lib/argocd.libjsonnet';

local app = argocd.App('talos-backup', params.namespace);
local app = argocd.App('talos-backup', params.namespace.name) {
spec+: {
syncPolicy+: {
syncOptions+: [
'ServerSideApply=true',
],
},
},
};

local appPath =
local project = std.get(std.get(app, 'spec', {}), 'project', 'syn');
Expand Down
65 changes: 63 additions & 2 deletions component/main.jsonnet
Original file line number Diff line number Diff line change
@@ -1,10 +1,71 @@
// main template for talos-backup
local com = import 'lib/commodore.libjsonnet';
local kap = import 'lib/kapitan.libjsonnet';
local kube = import 'lib/kube.libjsonnet';
local lib = import 'talos-backup.libsonnet';
local inv = kap.inventory();
// The hiera parameters for the component
local params = inv.parameters.talos_backup;

// Define outputs below
local componentName = inv.parameters._instance;

assert
std.length(params.age_recipient_public_keys) > 0
: 'talos_backup: age_recipient_public_keys must contain at least one public key';
assert
params.s3.bucket != ''
: 'talos_backup: s3.bucket must be set';
assert
!params.s3.credentials.create
|| (params.s3.credentials.access_key_id != '' && params.s3.credentials.secret_access_key != '')
: 'talos_backup: s3.credentials.create=true requires access_key_id and secret_access_key';

local commonLabels = {
'app.kubernetes.io/component': componentName,
'app.kubernetes.io/managed-by': 'commodore',
'app.kubernetes.io/part-of': 'syn',
};

local commonMetadata = {
labels+: commonLabels,
};

local namespace = kube.Namespace(params.namespace.name) {
metadata: commonMetadata + com.makeMergeable({
name: params.namespace.name,
annotations: params.namespace.annotations,
labels: params.namespace.labels {
'app.kubernetes.io/name': params.namespace.name,
},
}),
};

local talosServiceAccount = lib.TalosServiceAccount() {
metadata: commonMetadata + com.makeMergeable({
name: params.talos_service_account.name,
namespace: params.namespace.name,
labels: { 'app.kubernetes.io/name': params.talos_service_account.name },
}),
};

local cronjob = lib.CronJob(params) {
metadata: commonMetadata + com.makeMergeable({
name: componentName,
namespace: params.namespace.name,
labels: { 'app.kubernetes.io/name': componentName },
}),
};

local s3Secret = lib.S3CredentialsSecret(params) {
metadata: commonMetadata + com.makeMergeable({
name: params.s3.credentials.name,
namespace: params.namespace.name,
labels: { 'app.kubernetes.io/name': params.s3.credentials.name },
}),
};

{
'00_namespace': namespace,
'10_talos_serviceaccount': talosServiceAccount,
[if params.s3.credentials.create then '10_s3_credentials']: s3Secret,
'20_cronjob': cronjob,
}
120 changes: 120 additions & 0 deletions component/talos-backup.libsonnet
Comment thread
simu marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Library with public helper methods provided by component talos-backup.
*/

local kube = import 'lib/kube.libjsonnet';

local talosApiGroup = 'talos.dev';

local TalosServiceAccount() = {
apiVersion: '%s/v1alpha1' % talosApiGroup,
kind: 'ServiceAccount',
spec: {
roles: [ 'os:etcd:backup' ],
},
};

local S3CredentialsSecret(params) = kube.Secret(params.s3.credentials.name) {
stringData: {
AWS_ACCESS_KEY_ID: params.s3.credentials.access_key_id,
AWS_SECRET_ACCESS_KEY: params.s3.credentials.secret_access_key,
},
};

local image(params) =
'%(registry)s/%(repository)s:%(tag)s' % params.images.talos_backup;

local envFromParams(params) =
local fromSecret(k) = {
name: k,
valueFrom: {
secretKeyRef: {
name: params.s3.credentials.name,
key: k,
},
},
};
local kv(k, v) = { name: k, value: std.toString(v) };
local optional(k, v) = if v != '' && v != null then [ kv(k, v) ] else [];
[
fromSecret('AWS_ACCESS_KEY_ID'),
fromSecret('AWS_SECRET_ACCESS_KEY'),
kv('AWS_REGION', params.s3.region),
kv('BUCKET', params.s3.bucket),
kv('USE_PATH_STYLE', params.s3.use_path_style),
kv('ENABLE_COMPRESSION', params.enable_compression),
// Set both env vars to the same value: upstream Split("", ",") returns
// [""] from an unset var, poisoning the recipient list. Duplicates harmless.
kv('AGE_RECIPIENT_PUBLIC_KEY', std.join(',', params.age_recipient_public_keys)),
kv('AGE_X25519_PUBLIC_KEY', std.join(',', params.age_recipient_public_keys)),
]
+ optional('CUSTOM_S3_ENDPOINT', params.s3.endpoint)
+ optional('S3_PREFIX', params.s3.prefix)
+ optional('CLUSTER_NAME', params.cluster_name)
+ [ kv(k, params.extra_env[k]) for k in std.objectFields(params.extra_env) ];

local CronJob(params) = {
apiVersion: 'batch/v1',
kind: 'CronJob',
spec: {
schedule: params.schedule,
concurrencyPolicy: params.concurrency_policy,
successfulJobsHistoryLimit: params.successful_jobs_history_limit,
failedJobsHistoryLimit: params.failed_jobs_history_limit,
jobTemplate: {
spec: {
template: {
spec: {
restartPolicy: 'OnFailure',
automountServiceAccountToken: false,
securityContext: {
runAsNonRoot: true,
runAsUser: 1000,
runAsGroup: 1000,
fsGroup: 1000,
seccompProfile: { type: 'RuntimeDefault' },
},
[if params.node_selector != {} then 'nodeSelector']: params.node_selector,
[if params.tolerations != [] then 'tolerations']: params.tolerations,
[if params.affinity != {} then 'affinity']: params.affinity,
containers: [ {
name: 'talos-backup',
image: image(params),
imagePullPolicy: params.images.talos_backup.pull_policy,
workingDir: '/tmp',
command: [ '/talos-backup' ],
env: envFromParams(params),
resources: params.resources,
securityContext: {
allowPrivilegeEscalation: false,
readOnlyRootFilesystem: true,
capabilities: { drop: [ 'ALL' ] },
},
volumeMounts: [
{ mountPath: '/tmp', name: 'tmp' },
{ mountPath: '/.talos', name: 'talos' },
{ mountPath: '/var/run/secrets/talos.dev', name: 'talos-secrets' },
],
} ],
volumes: [
{ name: 'tmp', emptyDir: {} },
{ name: 'talos', emptyDir: {} },
{
name: 'talos-secrets',
secret: { secretName: params.talos_service_account.name },
},
],
},
},
},
},
},
};

{
TalosServiceAccount: TalosServiceAccount,
S3CredentialsSecret: S3CredentialsSecret,
CronJob: CronJob,

talosApiGroup: talosApiGroup,
}
Empty file.
37 changes: 34 additions & 3 deletions docs/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
= Talos Linux aware etcd snapshotter.
= talos-backup

talos-backup is a Commodore component to manage Talos Linux aware etcd snapshotter..
talos-backup is a Commodore component that deploys
https://github.com/siderolabs/talos-backup[siderolabs/talos-backup]
as a Kubernetes `CronJob` on a Talos Linux cluster.
It periodically snapshots etcd through the Talos API, encrypts the snapshot
with https://age-encryption.org[age], and pushes it to an S3-compatible
object store.

See the xref:references/parameters.adoc[parameters] reference for further details.
== Prerequisites

The Talos machine configuration must allow the Kubernetes-side Talos API
access for the `os:etcd:backup` role in the namespace where the component
is deployed:

[source,yaml]
----
machine:
features:
kubernetesTalosAPIAccess:
enabled: true
allowedRoles:
- os:etcd:backup
allowedKubernetesNamespaces:
- syn-talos-backup
----

You also need:

* An age keypair (`age-keygen`). The public key(s) are passed to the
component; the private key is kept by the operator to decrypt backups.
* An S3 bucket and credentials. A lifecycle policy on the bucket is the
recommended way to enforce retention.

See the xref:references/parameters.adoc[parameters] reference for the full
list of configurable options.
Loading
Loading