diff --git a/README.md b/README.md index 5f33a42..c5af7c7 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,91 @@ -Pykraken -========= +# ansible-role-kraken-bot -Deploys a Kubernetes CronJob for scheduled execution of pykraken. +Deploys the current Kraken Trader container as a continuously running Kubernetes +Deployment. The role creates a namespace, PVC, ConfigMap, Kubernetes Secret, +Deployment, and internal ClusterIP Service. It does not use a CronJob. -Requirements ------------- +The Deployment is fixed at one replica: multiple pods could place duplicate trades +and cannot safely share the SQLite state file. -An API token and secret from Kraken. +## Requirements -Role Variables --------------- +- Ansible 2.14 or newer. +- The `kubernetes.core` collection and Python `kubernetes` package on the Ansible + controller. Install the collection with `ansible-galaxy collection install -r requirements.yml`. +- A configured Kubernetes context, or set `kraken_bot_kubeconfig` and optionally + `kraken_bot_kube_context`. +- A published app image and dashboard password at least 16 characters long. -kraken_api_token: "..." -kraken_api_sec: "..." +## Example playbook -pykraken_namespace: pykraken -pykraken_schedule: "0 * * * * " -pykraken_r_value_target: .33 -pykraken_investment_count: 3 -pykraken_investment_volume: 100 +```yaml +- name: Deploy Kraken Trader + hosts: localhost + gather_facts: false + vars: + kraken_bot_image: ghcr.io/example/kraken-trader:2.0.0 + kraken_bot_dashboard_password: "{{ vault_kraken_dashboard_password }}" + kraken_bot_api_key: "{{ vault_kraken_api_key | default('') }}" + kraken_bot_api_secret: "{{ vault_kraken_api_secret | default('') }}" + kraken_bot_config: + poll_interval_seconds: 60 + snapshot_interval_seconds: 300 + fee_rate: 0.004 + slippage: 0.0005 + min_trade_fraction: 0.01 + strategies: + - id: hodl-btc + type: buy_and_hold + mode: paper + starting_capital: 1000 + pairs: [XBTUSD] + interval: 1440 + roles: + - ansible-role-kraken-bot +``` -Dependencies ------------- +Store passwords and Kraken credentials in Ansible Vault or an external secret +manager. Do not commit plaintext credentials to playbooks or inventory. -None. +## Variables -Example Playbook ----------------- +| Variable | Default | Description | +|---|---|---| +| `kraken_bot_image` | required | Full published image reference, preferably pinned to a version or digest. | +| `kraken_bot_namespace` | `kraken-trader` | Namespace to create and deploy into. | +| `kraken_bot_name` | `kraken-trader` | Resource name prefix. | +| `kraken_bot_dashboard_password` | required | Dashboard Basic Auth password (minimum 16 characters). | +| `kraken_bot_dashboard_username` | `admin` | Dashboard Basic Auth username. | +| `kraken_bot_api_key` / `kraken_bot_api_secret` | empty | Kraken credentials. Read-only keys display account values; live trading needs order permission. Never enable withdrawals. | +| `kraken_bot_live_trading_enabled` | `false` | Explicit live-trading guard. Live mode requires both Kraken credentials. | +| `kraken_bot_config` | paper `buy_and_hold` example | Full application `strategies.yaml` content as a mapping. | +| `kraken_bot_storage_size` | `1Gi` | Persistent volume request. | +| `kraken_bot_storage_class` | cluster default | Optional storage class. | +| `kraken_bot_service_type` | `ClusterIP` | Keep internal unless adding a secured ingress. | +| `kraken_bot_kubeconfig` | current context | Optional kubeconfig path. | +| `kraken_bot_kube_context` | current context | Optional kubeconfig context. | +| `kraken_bot_image_pull_policy` | `IfNotPresent` | Kubernetes image pull policy. | +| `kraken_bot_log_level` | `INFO` | Application log level. | - - hosts: servers - roles: - - { role: ericomeehan.kraken-bot } +The role rejects multiple replicas, short or missing dashboard passwords, and +live strategies unless live trading is explicitly enabled and credentials are +present. Trading remains in paper mode by default. -License -------- +## Network and secrets -BSD +The Service is `ClusterIP`; the dashboard requires HTTP Basic authentication. +Use TLS at any ingress or reverse proxy; do not expose Basic Auth over plain HTTP. +`/healthz` is unauthenticated for Kubernetes probes. Kubernetes Secret values are +stored in etcd, so configure encryption at rest and restrict namespace access. +Secret rotation requires restarting the Deployment so environment variables +refresh. -Author Information ------------------- +## Configuration updates -Eric O'Neill Meehan +Re-run the role after editing `kraken_bot_config`. A checksum annotation rolls +pods when the strategy config changes. The SQLite database remains on the PVC; +changing a strategy ID creates a separate portfolio rather than resetting state. + +## License + +BSD-3-Clause diff --git a/defaults/main.yml b/defaults/main.yml index b50a0b9..0ffcac7 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,9 +1,35 @@ --- -# defaults file for ansible-role-kraken-bot -kraken_bot_namespace: "kraken" -kraken_bot_schedule: "0 0 * * *" -kraken_bot_r_value_target: ".33" -kraken_bot_investment_count: "3" -kraken_bot_investment_volume: "10" -kraken_bot_target_gain: ".15" -kraken_bot_max_loss: ".05" +kraken_bot_namespace: kraken-trader +kraken_bot_name: kraken-trader +kraken_bot_image: "" +kraken_bot_image_pull_policy: IfNotPresent + +kraken_bot_replicas: 1 +kraken_bot_storage_size: 1Gi +kraken_bot_storage_class: "" +kraken_bot_service_type: ClusterIP + +kraken_bot_kubeconfig: "" +kraken_bot_kube_context: "" + +kraken_bot_api_key: "" +kraken_bot_api_secret: "" +kraken_bot_dashboard_username: admin +kraken_bot_dashboard_password: "" +kraken_bot_live_trading_enabled: false +kraken_bot_log_level: INFO + +kraken_bot_config: + poll_interval_seconds: 60 + snapshot_interval_seconds: 300 + fee_rate: 0.004 + slippage: 0.0005 + min_trade_fraction: 0.01 + strategies: + - id: hodl-btc + type: buy_and_hold + mode: paper + starting_capital: 1000 + pairs: + - XBTUSD + interval: 1440 diff --git a/handlers/main.yml b/handlers/main.yml deleted file mode 100644 index fdd6e51..0000000 --- a/handlers/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# handlers file for ansible-role-kraken-bot diff --git a/meta/main.yml b/meta/main.yml index c572acc..bf75b06 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,52 +1,15 @@ +--- galaxy_info: - author: your name - description: your role description - company: your company (optional) - - # If the issue tracker for your role is not on github, uncomment the - # next line and provide a value - # issue_tracker_url: http://example.com/issue/tracker - - # Choose a valid license ID from https://spdx.org - some suggested licenses: - # - BSD-3-Clause (default) - # - MIT - # - GPL-2.0-or-later - # - GPL-3.0-only - # - Apache-2.0 - # - CC-BY-4.0 - license: license (GPL-2.0-or-later, MIT, etc) - - min_ansible_version: 2.1 - - # If this a Container Enabled role, provide the minimum Ansible Container version. - # min_ansible_container_version: - - # - # Provide a list of supported platforms, and for each platform a list of versions. - # If you don't wish to enumerate all versions for a particular platform, use 'all'. - # To view available platforms and versions (or releases), visit: - # https://galaxy.ansible.com/api/v1/platforms/ - # - # platforms: - # - name: Fedora - # versions: - # - all - # - 25 - # - name: SomePlatform - # versions: - # - all - # - 1.0 - # - 7 - # - 99.99 - - galaxy_tags: [] - # List tags for your role here, one per line. A tag is a keyword that describes - # and categorizes the role. Users find roles by searching for tags. Be sure to - # remove the '[]' above, if you add tags to this list. - # - # NOTE: A tag is limited to a single word comprised of alphanumeric characters. - # Maximum 20 tags per role. + author: Eric O'Neill Meehan + description: Deploy the continuous Kraken Trader portfolio manager to Kubernetes + license: BSD-3-Clause + min_ansible_version: "2.14" + platforms: [] + galaxy_tags: + - kubernetes + - kraken + - cryptocurrency + - trading dependencies: [] - # List your role dependencies here, one per line. Be sure to remove the '[]' above, - # if you add dependencies to this list. + diff --git a/meta/runtime.yml b/meta/runtime.yml new file mode 100755 index 0000000..59a8cbf --- /dev/null +++ b/meta/runtime.yml @@ -0,0 +1,2 @@ +--- +requires_ansible: ">=2.14.0" diff --git a/requirements.yml b/requirements.yml new file mode 100755 index 0000000..6824101 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: kubernetes.core + version: ">=3.0.0,<7.0.0" diff --git a/tasks/main.yml b/tasks/main.yml index 4ca63fe..669c886 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,49 +1,226 @@ --- -# tasks file for ansible-role-kraken-bot -- name: Create kraken-bot namespace - k8s: +- name: Validate deployment settings + ansible.builtin.assert: + that: + - kraken_bot_image | length > 0 + - kraken_bot_dashboard_password | length >= 16 + - kraken_bot_replicas | int == 1 + - kraken_bot_config is mapping + - kraken_bot_config.strategies is defined + - >- + (not (kraken_bot_live_trading_enabled | bool)) or + ((kraken_bot_api_key | length > 0) and (kraken_bot_api_secret | length > 0)) + - >- + (kraken_bot_live_trading_enabled | bool) or + (kraken_bot_config.strategies | selectattr('mode', 'defined') + | selectattr('mode', 'equalto', 'live') | list | length == 0) + fail_msg: >- + Set kraken_bot_image and a dashboard password of at least 16 characters. + Live mode additionally requires kraken_bot_live_trading_enabled=true and + Kraken API credentials. This role only supports one replica because all + strategies share the same account and persistent state. + +- name: Create application namespace + kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Namespace metadata: name: "{{ kraken_bot_namespace }}" + labels: + app.kubernetes.io/name: "{{ kraken_bot_name }}" + app.kubernetes.io/managed-by: ansible + kubeconfig: "{{ kraken_bot_kubeconfig | default(omit, true) }}" + context: "{{ kraken_bot_kube_context | default(omit, true) }}" -- name: Create a cron job for kraken-bot - k8s: +- name: Create persistent application data volume + kubernetes.core.k8s: state: present definition: - apiVersion: batch/v1 - kind: CronJob + apiVersion: v1 + kind: PersistentVolumeClaim metadata: - name: kraken-bot + name: "{{ kraken_bot_name }}-data" namespace: "{{ kraken_bot_namespace }}" spec: - schedule: "{{ kraken_bot_schedule }}" - jobTemplate: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "{{ kraken_bot_storage_size }}" + storageClassName: "{{ kraken_bot_storage_class | default(omit, true) }}" + kubeconfig: "{{ kraken_bot_kubeconfig | default(omit, true) }}" + context: "{{ kraken_bot_kube_context | default(omit, true) }}" + +- name: Create strategy configuration + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: "{{ kraken_bot_name }}-config" + namespace: "{{ kraken_bot_namespace }}" + data: + strategies.yaml: "{{ kraken_bot_config | to_nice_yaml(indent=2, sort_keys=false) }}" + kubeconfig: "{{ kraken_bot_kubeconfig | default(omit, true) }}" + context: "{{ kraken_bot_kube_context | default(omit, true) }}" + +- name: Create credentials secret + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ kraken_bot_name }}-secrets" + namespace: "{{ kraken_bot_namespace }}" + type: Opaque + stringData: + KRAKEN_API_KEY: "{{ kraken_bot_api_key }}" + KRAKEN_API_SECRET: "{{ kraken_bot_api_secret }}" + DASHBOARD_USERNAME: "{{ kraken_bot_dashboard_username }}" + DASHBOARD_PASSWORD: "{{ kraken_bot_dashboard_password }}" + kubeconfig: "{{ kraken_bot_kubeconfig | default(omit, true) }}" + context: "{{ kraken_bot_kube_context | default(omit, true) }}" + no_log: true + +- name: Deploy continuous trading service + kubernetes.core.k8s: + state: present + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: "{{ kraken_bot_name }}" + namespace: "{{ kraken_bot_namespace }}" + labels: + app.kubernetes.io/name: "{{ kraken_bot_name }}" + spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: "{{ kraken_bot_name }}" + template: + metadata: + labels: + app.kubernetes.io/name: "{{ kraken_bot_name }}" + annotations: + kraken-bot/config-sha256: "{{ kraken_bot_config | to_nice_yaml(indent=2, sort_keys=false) | hash('sha256') }}" spec: - template: - spec: - containers: - - name: bot - image: ericomeehan/kraken-bot - imagePullPolicy: Always - env: - - name: KRAKEN_API_TOKEN - value: "{{ kraken_api_token }}" - - name: KRAKEN_API_SEC - value: "{{ kraken_api_sec }}" - - name: R_VALUE_TARGET - value: "{{ kraken_bot_r_value_target }}" - - name: INVESTMENT_COUNT - value: "{{ kraken_bot_investment_count }}" - - name: INVESTMENT_VOLUME - value: "{{ kraken_bot_investment_volume }}" - - name: TARGET_GAIN - value: "{{ kraken_bot_target_gain }}" - - name: MAX_LOSS - value: "{{ kraken_bot_max_loss }}" - command: - - /usr/bin/python3 - - /usr/local/src/kraken-bot/app.py - restartPolicy: OnFailure + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + fsGroup: 10001 + seccompProfile: + type: RuntimeDefault + containers: + - name: kraken-trader + image: "{{ kraken_bot_image }}" + imagePullPolicy: "{{ kraken_bot_image_pull_policy }}" + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: CONFIG_PATH + value: /config/strategies.yaml + - name: DATA_DIR + value: /data + - name: HOST + value: 0.0.0.0 + - name: PORT + value: "8080" + - name: LIVE_TRADING_ENABLED + value: "{{ kraken_bot_live_trading_enabled | bool | string | lower }}" + - name: LOG_LEVEL + value: "{{ kraken_bot_log_level }}" + - name: KRAKEN_API_KEY + valueFrom: + secretKeyRef: + name: "{{ kraken_bot_name }}-secrets" + key: KRAKEN_API_KEY + - name: KRAKEN_API_SECRET + valueFrom: + secretKeyRef: + name: "{{ kraken_bot_name }}-secrets" + key: KRAKEN_API_SECRET + - name: DASHBOARD_USERNAME + valueFrom: + secretKeyRef: + name: "{{ kraken_bot_name }}-secrets" + key: DASHBOARD_USERNAME + - name: DASHBOARD_PASSWORD + valueFrom: + secretKeyRef: + name: "{{ kraken_bot_name }}-secrets" + key: DASHBOARD_PASSWORD + volumeMounts: + - name: data + mountPath: /data + - name: config + mountPath: /config + readOnly: true + - name: tmp + mountPath: /tmp + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 15 + resources: + requests: + cpu: 50m + memory: 256Mi + limits: + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumes: + - name: data + persistentVolumeClaim: + claimName: "{{ kraken_bot_name }}-data" + - name: config + configMap: + name: "{{ kraken_bot_name }}-config" + - name: tmp + emptyDir: {} + kubeconfig: "{{ kraken_bot_kubeconfig | default(omit, true) }}" + context: "{{ kraken_bot_kube_context | default(omit, true) }}" + +- name: Expose dashboard inside the cluster + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Service + metadata: + name: "{{ kraken_bot_name }}" + namespace: "{{ kraken_bot_namespace }}" + spec: + type: "{{ kraken_bot_service_type }}" + selector: + app.kubernetes.io/name: "{{ kraken_bot_name }}" + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP + kubeconfig: "{{ kraken_bot_kubeconfig | default(omit, true) }}" + context: "{{ kraken_bot_kube_context | default(omit, true) }}" diff --git a/tests/inventory b/tests/inventory deleted file mode 100644 index 878877b..0000000 --- a/tests/inventory +++ /dev/null @@ -1,2 +0,0 @@ -localhost - diff --git a/tests/test.yml b/tests/test.yml deleted file mode 100644 index e752832..0000000 --- a/tests/test.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -- hosts: localhost - remote_user: root - roles: - - ansible-role-kraken-bot diff --git a/vars/main.yml b/vars/main.yml deleted file mode 100644 index a15c803..0000000 --- a/vars/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# vars file for ansible-role-kraken-bot