Skip to content

Kubernetes

Run the importer as a CronJob (run-once on a schedule) or a Deployment (long-running with internal cron). The CronJob pattern is more idiomatic for Kubernetes and avoids the SCHEDULE env var.

Let Kubernetes own the schedule; let the importer be a one-shot job.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: israeli-bank-importer
spec:
  schedule: "0 */8 * * *"
  timeZone: "Asia/Jerusalem"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: OnFailure
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            runAsGroup: 1000
          containers:
            - name: importer
              image: sergienko4/israeli-bank-importer:latest
              imagePullPolicy: IfNotPresent
              securityContext:
                allowPrivilegeEscalation: false
                capabilities:
                  drop:
                    - ALL
                  add:
                    - SYS_ADMIN
                readOnlyRootFilesystem: false
              resources:
                requests:
                  memory: 512Mi
                  cpu: 250m
                limits:
                  memory: 2Gi
                  cpu: "1.5"
              env:
                - name: TZ
                  value: Asia/Jerusalem
                - name: CREDENTIALS_ENCRYPTION_PASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: importer-encryption
                      key: password
              volumeMounts:
                - name: config
                  mountPath: /app/config.json
                  subPath: config.json
                  readOnly: true
                - name: data
                  mountPath: /app/data
                - name: cache
                  mountPath: /app/cache
                - name: logs
                  mountPath: /app/logs
                - name: shm
                  mountPath: /dev/shm
          volumes:
            - name: config
              secret:
                secretName: importer-config
            - name: data
              persistentVolumeClaim:
                claimName: importer-data
            - name: cache
              emptyDir: {}
            - name: logs
              emptyDir: {}
            - name: shm
              emptyDir:
                medium: Memory
                sizeLimit: 256Mi

Pattern 2 — Deployment (long-running)

Use this if you want to keep the Telegram bot's long-polling alive 24/7.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: israeli-bank-importer
spec:
  replicas: 1
  strategy:
    type: Recreate          # never run two pollers at once
  selector:
    matchLabels: { app: israeli-bank-importer }
  template:
    metadata:
      labels: { app: israeli-bank-importer }
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
      containers:
        - name: importer
          image: sergienko4/israeli-bank-importer:latest
          env:
            - name: TZ
              value: Asia/Jerusalem
            - name: SCHEDULE
              value: "0 */8 * * *"
            - name: CREDENTIALS_ENCRYPTION_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: importer-encryption
                  key: password
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: [ALL]
              add: [SYS_ADMIN]
          resources:
            requests: { memory: 512Mi, cpu: 250m }
            limits:   { memory: 2Gi,   cpu: "1.5" }
          volumeMounts:
            - { name: config, mountPath: /app/config.json, subPath: config.json, readOnly: true }
            - { name: data,   mountPath: /app/data }
            - { name: cache,  mountPath: /app/cache }
            - { name: logs,   mountPath: /app/logs }
            - { name: shm,    mountPath: /dev/shm }
      volumes:
        - name: config
          secret: { secretName: importer-config }
        - name: data
          persistentVolumeClaim: { claimName: importer-data }
        - name: cache
          emptyDir: {}
        - name: logs
          emptyDir: {}
        - name: shm
          emptyDir: { medium: Memory, sizeLimit: 256Mi }

Resource limits

Both patterns set resources.limits.memory: 2Gi. Keep it. A stalled bank scrape can strand a browser process; without a limit the pod keeps allocating node RAM until the kubelet evicts unrelated workloads or the node stops responding. With the limit, only this pod is OOM-killed.

The 2 Gi figure is measured: ~200 MB Node baseline + ~1 GB worst-case bank browser + ~300 MB receipt OCR + headroom. Setting NODE_OPTIONS=--max-old-space-size instead does not work — it caps only the JS heap, while most of the footprint is native browser memory.

Secrets

Use Sealed Secrets or SOPS for credentials in Git. Never commit raw config.json.

# Plain config-from-file (development)
kubectl create secret generic importer-config \
  --from-file=config.json=./config.json

# Production: encrypt with sealed-secrets or sops first

Health and observability

  • Container logs → stdout (logConfig.format: "json" for Loki/ELK ingestion).
  • Annotate the namespace for Prometheus pod-scrape if you front the importer with a sidecar metrics exporter (not bundled — see Roadmap).

See also