Прийшла задачка підняти для проекту цікавий сервіс Arize Phoenix для моніторингу і тюнингу використання LLM.
За сам сервіс багато не скажу, бо не користувався, але його запуск вийшов доволі цікавим.
Що будемо робити – спочатку з Helm запустимо тестовий варіант, подивитись як воно взагалі виглядає, потім зробимо повноцінну автоматизацію – Terraform для всяких сікретів, Helm для самого Phoenix.
Власне цей пост буде не стільки про сам Arize Phoenix, скільки просто приклад як з Terraform створити AWS Secrets, і як з Helm та External Secrets Operator ці сікрети отримати.
Зміст
Тестовий запуск з Helm в Kubernetes
Phoenix підтримує різні варіанти запуску. нам цікавий Helm, документація тут – Kubernetes (helm).
Сам чарт є в Docker Hub (і далі це трохи вилізе боком), всі values є там жеж.
Можемо спулити чарт собі локально:
$ helm pull $CHART_URL Pulled: registry-1.docker.io/arizephoenix/phoenix-helm:4.0.4 Digest: sha256:c5692ed16ea9de346e91181c1afc2a0294af0b7f9e3dc3e13d663ee4a00ace1e
Розпаковуваємо:
$ tar xfp phoenix-helm-4.0.4.tgz
І дивимось файли. Далі довелось полазити в них, або зрозуміти логіку.
Або шукаємо в GitHub тут>>>.
Створюємо Kubernetes Namespace:
$ kk create ns test-phoenix-ns namespace/test-phoenix-ns created
Встановлюємо чарт:
$ export CHART_URL=oci://registry-1.docker.io/arizephoenix/phoenix-helm $ helm -n test-phoenix-ns install phoenix $CHART_URL --debug
Перевіряємо сервіси:
$ kk get all NAME READY STATUS RESTARTS AGE pod/phoenix-8677bcc44f-k8w2k 1/1 Running 1 (49s ago) 70s pod/phoenix-postgresql-0 1/1 Running 0 70s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/phoenix-postgresql ClusterIP 172.20.11.177 <none> 5432/TCP 70s service/phoenix-svc NodePort 172.20.85.64 <none> 4317:31314/TCP,6006:31180/TCP,9090:31897/TCP 70s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/phoenix 1/1 1 1 70s NAME DESIRED CURRENT READY AGE replicaset.apps/phoenix-8677bcc44f 1 1 1 70s NAME READY AGE statefulset.apps/phoenix-postgresql 1/1 71s
По дефолту використовує власний контейнер з PostgreSQL, для Production будемо робити в AWS RDS.
Відкриваємо доступ до порту для WebUI:
$ kk port-forward service/phoenix-svc 6006
Переходимо в браузері на http://localhost:6006, логінимось.
Дефолтний логін – admin@localhost
, пароль – admin
.
Документація по аутентифікація – тут>>>, і там є цікаві моменти. наприклад, змінити пошту для адміна (і для Member? тобто для звичайних юзерів? не пробував) не міжна:
Neither an Admin nor Member is permitted to change email addresses.
ОК, воно працює – давайте думати про продакшен сетап.
AWS та Terraform
Що нам треба буде з ресурсів в AWS:
- запис Route 53 з доменом для доступу юзерів
- TLS сертифікат в AWS Certificate Manager
- AWS Secrets Manager:
- пароль для доступу до Postgres
- два паролі для самого Phoenix
- пароль для SMTP – навіть якщо він не використовується
Готуємо файл backend.tf
:
terraform { backend "s3" { bucket = "tf-state-backend-atlas-phoenix" use_lockfile = true region = "us-east-1" encrypt = true } }
Готуємо файли variables.tf
, providers.tf
, versions.tf
, outputs.tf
.
В результаті в мене виходить така структура – стандартна в нашому проекті:
$ tree . . ├── Makefile ├── backend.tf ├── data.tf ├── envs │ └── ops │ └── ops-1-33.tfvars ├── outputs.tf ├── providers.tf ├── variables.tf └── versions.tf
Тут “ops” – це ім’я AWS-оточення, а в ops-1-33.tfvars
значення специфічні для поточного кластеру AWS Elasctic Kubernetes Service.
Запис в AWS Route 53
Додаємо нову змінну:
variable "dns_zone" { description = "AWS Route 53 zone for the AWS Ops environment" type = string default = "ops.example.co" }
В файл data.tf
додаємо отримання інформації про зону:
data "aws_route53_zone" "ops" { name = var.dns_zone }
Для запису в Route 53 треба буде створити CNAME
на AWS Application Load Balancer.
У нас використовується один external ALB для всіх сервісів в Kubernetes, див. Kubernetes: єдиний AWS Load Balancer для різних Kubernetes Ingress.
Тому просто отримаємо інформацію по ньому з ще одним ресурсом data
.
Додаємо змінну з іменем ALB:
variable "aws_alb_name" { description = "AWS EKS Shared Load Balancer name specific to EKS Environment" type = string }
Додаємо значення в ops-1-33.tfvars
:
aws_alb_name = "k8s-ops133externalalb-***"
І додаємо data
:
data "aws_lb" "shared_alb" { name = var.aws_alb_name }
В файлі locals.tf
створимо нову local
з повним іменем:
locals { # 'phoenix.ops.example.co' phoenix_domain_name = "phoenix.${var.dns_zone}" }
І тепер можемо описати новий record в Route 53:
resource "aws_route53_record" "phoenix_dns" { zone_id = data.aws_route53_zone.ops.zone_id name = local.phoenix_domain_name type = "CNAME" ttl = 300 records = [ data.aws_lb.shared_alb.dns_name ] }
Виконуємо terraform init
та terraform plan
, перевіряємо, що все ок:
... Terraform will perform the following actions: # aws_route53_record.phoenix_dns will be created + resource "aws_route53_record" "phoenix_dns" { + allow_overwrite = (known after apply) + fqdn = (known after apply) + id = (known after apply) + name = "phoenix.ops.example.co" + records = [ + "k8s-ops133externalalb-***.us-east-1.elb.amazonaws.com", ] + ttl = 300 + type = "CNAME" + zone_id = "Z02***OYY" } Plan: 1 to add, 0 to change, 0 to destroy.
Сертифікат в AWS ACM
Далі для Ingress та ALB нам потрібно створити сертифікат під цей DNS:
module "ops_phoenix_acm" { source = "terraform-aws-modules/acm/aws" version = "~> 6.0" # 'phoenix.ops.example.co' domain_name = local.phoenix_domain_name zone_id = data.aws_route53_zone.ops.zone_id validation_method = "DNS" wait_for_validation = true tags = { Name = local.phoenix_domain_name } }
Записи в AWS Secrets Manager
Сікретів для Phoenix нас буде кілька:
PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD
: пароль при сетапіPHOENIX_ADMIN_SECRET
: пароль після сетапу- чесно тут не дуже зрозумів, бо навіть якщо відразу створити і передати
PHOENIX_ADMIN_SECRET
– то перший логін все одно буде зPHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD
- чесно тут не дуже зрозумів, бо навіть якщо відразу створити і передати
PHOENIX_SECRET
: для підпису JWT-токенів (писав давно, але все ще актуально – Kubernetes: ServiceAccounts, JWT-tokens, authentication, and RBAC authorization)PHOENIX_POSTGRES_PASSWORD
: пароль доступу до сервера баз даних
Сікрети в AWS будемо робити з ephemeral та write-only, див. Terraform: використання Ephemeral resources та Write-only attributes.
Описуємо перший сікрет:
# auth.defaultAdminPassword or PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD # PHOENIX_ADMIN_SECRET # PHOENIX_SECRET: A long string value that is used to sign JWTs for your deployment. # PHOENIX_POSTGRES_PASSWORD # PHOENIX_SMTP_PASSWORD ############################################## ### PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD ### ############################################## # generate a random password ephemeral "random_password" "ops_phoenix_default_admin_initail_secret_random_password" { length = 12 special = true } # create an AWS Secret resource resource "aws_secretsmanager_secret" "ops_phoenix_default_admin_initial_secret" { name = "/ops/phoenix/phoenix_default_admin_initial_secret" description = "Default Phoenix admin username and password" recovery_window_in_days = 0 } # create an AWS Secret value resource "aws_secretsmanager_secret_version" "ops_phoenix_default_admin_initial_secret_version" { secret_id = aws_secretsmanager_secret.ops_phoenix_default_admin_initial_secret.id secret_string_wo = ephemeral.random_password.ops_phoenix_default_admin_initail_secret_random_password.result secret_string_wo_version = 1 }
Деплоїмо, перевіряємо Route 53, ACM та Secrets Manager:
Повторюємо для решти – вони всі більш-менш однакові, тільки в деяких просто пароль, в деяких логін:пароль в JSON, і різна довжина.
Бо, наприклад, для PHOENIX_ADMIN_SECRET
є перевірка на кількість символів:
... atlas-phoenix-6865f69ffc-k7hwl:phoenix File "/phoenix/env/phoenix/config.py", line 772, in get_env_phoenix_admin_secret atlas-phoenix-6865f69ffc-k7hwl:phoenix REQUIREMENTS_FOR_PHOENIX_SECRET.validate(phoenix_admin_secret, "Phoenix secret") atlas-phoenix-6865f69ffc-k7hwl:phoenix File "/phoenix/env/phoenix/auth.py", line 255, in validate atlas-phoenix-6865f69ffc-k7hwl:phoenix raise ValueError(err_text) atlas-phoenix-6865f69ffc-k7hwl:phoenix ValueError: Phoenix secret must be at least 32 characters long ....
Описуємо ресурси:
... ############################ ### PHOENIX_ADMIN_SECRET ### ############################ # generate a random password ephemeral "random_password" "ops_phoenix_admin_secret_random_password" { length = 32 special = true } # create an AWS Secret resource resource "aws_secretsmanager_secret" "ops_phoenix_admin_secret" { name = "/ops/phoenix/phoenix_admin_secret" description = "Phoenix admin username and password" recovery_window_in_days = 0 } # create an AWS Secret value resource "aws_secretsmanager_secret_version" "ops_phoenix_admin_secret_version" { secret_id = aws_secretsmanager_secret.ops_phoenix_admin_secret.id secret_string_wo = jsonencode({ login = "admin@localhost" password = ephemeral.random_password.ops_phoenix_admin_secret_random_password.result }) secret_string_wo_version = 3 } ###################### ### PHOENIX_SECRET ### ###################### # generate a random password ephemeral "random_password" "ops_phoenix_secret_random_password" { length = 65 special = false } # create an AWS Secret resource resource "aws_secretsmanager_secret" "ops_phoenix_secret" { name = "/ops/phoenix/phoenix_secret" description = "Phoenix secret string used to sign JWTs" recovery_window_in_days = 0 } # create an AWS Secret value resource "aws_secretsmanager_secret_version" "ops_phoenix_secret_version" { secret_id = aws_secretsmanager_secret.ops_phoenix_secret.id secret_string_wo = ephemeral.random_password.ops_phoenix_secret_random_password.result secret_string_wo_version = 1 } ################################# ### PHOENIX_POSTGRES_PASSWORD ### ################################# # generate a random password ephemeral "random_password" "ops_phoenix_postgres_random_password" { length = 12 special = false } # create an AWS Secret resource resource "aws_secretsmanager_secret" "ops_phoenix_postgres_credentials" { name = "/ops/phoenix/phoenix_postgres_credentials" description = "Phoenix PostgreSQL username and password" recovery_window_in_days = 0 } # create an AWS Secret value resource "aws_secretsmanager_secret_version" "ops_phoenix_postgres_credentials_version" { secret_id = aws_secretsmanager_secret.ops_phoenix_postgres_credentials.id secret_string_wo = ephemeral.random_password.ops_phoenix_postgres_random_password.result secret_string_wo_version = 3 } ############################# ### PHOENIX_SMTP_PASSWORD ### ############################# # generate a random password ephemeral "random_password" "ops_phoenix_smtp_password_random_password" { length = 12 special = false } # create an AWS Secret resource resource "aws_secretsmanager_secret" "ops_phoenix_smtp_password" { name = "/ops/phoenix/ops_phoenix_smtp_password" description = "Phoenix secret string used to sign JWTs" recovery_window_in_days = 0 } # create an AWS Secret value resource "aws_secretsmanager_secret_version" "ops_phoenix_smtp_password_version" { secret_id = aws_secretsmanager_secret.ops_phoenix_smtp_password.id secret_string_wo = ephemeral.random_password.ops_phoenix_smtp_password_random_password.result secret_string_wo_version = 2 }
З Terraform все, можемо готувати базу Postgres.
PostgreSQL user and database
Сервер у нас вже є, тому зараз просто створити базу і юзера.
Підключаємось до RDS:
$ export PGPASSWORD='***' $ psql -h db.monitoring.ops.example.co -U ops_monitoring_user -d ops_grafana_db psql (17.6, server 16.8) ... ops_grafana_db=>
Створюємо юзера, базу, даємо повний доступ до цієї бази:
ops_grafana_db=> CREATE USER ops_phoenix_user WITH PASSWORD '***'; CREATE ROLE ops_grafana_db=> CREATE DATABASE ops_phoenix_db OWNER ops_phoenix_user; CREATE DATABASE ops_grafana_db=> GRANT ALL PRIVILEGES ON DATABASE ops_phoenix_db TO ops_phoenix_user; GRANT
І тепер саме цікаве – Helm.
Деплой Helm
Для отримання паролів з AWS Secrets Manager будемо використовувати External Secrets Operator (див. AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager), для цього нам треба буде в чарт додати власні файли.
Тому робимо новий чарт в якому через Helm Dependency використовуємо чарт Arize Phoenix.
Описуємо Chart.yaml
– і отут буде проблема з Docker Hub, див. далі.
Пишемо файл:
apiVersion: v2 name: atlas-phoenix description: A Helm chart for Arize Phoenix stack type: application version: 0.1.1 appVersion: "1.17.0" dependencies: - name: phoenix version: ~4.0 repository: oci://registry-1.docker.io/arizephoenix/phoenix-helm
Тепер робимо helm dependency update
, і ловимо “response status code 401” від Docker Hub:
... Update Complete. ⎈Happy Helming!⎈ Error: could not retrieve list of tags for repository oci://registry-1.docker.io/arizephoenix/phoenix-helm: GET "https://registry-1.docker.io/v2/arizephoenix/phoenix-helm/phoenix/tags/list": response status code 401: unauthorized: authentication required: [map[Action:pull Class: Name:arizephoenix/phoenix-helm/phoenix Type:repository]]
Тому що Helm при dependency update
намагається отримати всі доступні теги з tags/list
, а в Docker Hub для цього потрібно залогінитись.
Логінитись туди я і не хочу, і це зламає можилу майбутню автоматизацію, тому робимо костиль.
Пишемо Makefile в якому додаємо таргет на helm pull oci://
:
helm-oci-pull: mkdir -p charts/ && cd charts/ && helm pull oci://registry-1.docker.io/arizephoenix/phoenix-helm \ --version 4.0.4 \ --untar helm-template-ops-1-33: helm -n ops-phoenix-ns template .
Редагуємо Chart.yml
, в repository задаємо значення з file://charts/
:
apiVersion: v2 name: atlas-phoenix description: A Helm chart for Arize Phoenix stack type: application version: 0.1.1 appVersion: "1.17.0" dependencies: - name: phoenix-helm repository: file://charts/phoenix
Пулимо чарт:
$ make helm-oci-pull mkdir -p charts/ && cd charts/ && helm pull oci://registry-1.docker.io/arizephoenix/phoenix-helm \ --version 4.0.4 \ --untar Pulled: registry-1.docker.io/arizephoenix/phoenix-helm:4.0.4 Digest: sha256:c5692ed16ea9de346e91181c1afc2a0294af0b7f9e3dc3e13d663ee4a00ace1e
І перевіримо, що все нормально рендериться:
$ make helm-template-ops-1-33 helm -n ops-phoenix-ns template . --- # Source: atlas-phoenix/charts/phoenix-helm/charts/postgresql/templates/secureconfig.yaml apiVersion: v1 kind: Secret metadata: name: release-name-postgresql labels: helm.sh/chart: postgresql-1.5.8 app.kubernetes.io/name: postgresql app.kubernetes.io/instance: release-name app.kubernetes.io/version: "17.6" app.kubernetes.io/managed-by: Helm ...
Додавання values
Створюємо директорії і файл з параметрами для поточного кластеру EKS 1.33:
$ mkdir -p values/ops $ touch values/ops/atlas-phoenix-ops-1-33-values.yaml
Заносимо значення – і власні, далі їх будемо використовувати, і для phoenix-helm
:
aws: region: "us-east-1" config: env: "ops" phoenix-helm: auth: # Kubernetes Secret name name: phoenix-secret
Kubernetes Secrets з External Secrets Operator
Створюємо каталог для власних файлів і файл для ESO SecretStore:
$ mkdir templates/ $ touch templates/secretstore.yaml
Описуємо SecretStore та ExternalSecret, який створить Kubernetes Secret з ім’ям phoenix-secret
:
apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: phoenix-secret-store spec: provider: aws: service: SecretsManager region: {{ .Values.aws.region }} --- # the ExternalSecret resource is used to: # 1. authentificate in AWS using the SecretStore defined above # 2. get data from the AWS ParameterStore # 3. create a Kubernetes Secret apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: phoenix-external-secret spec: refreshInterval: 5m secretStoreRef: name: phoenix-secret-store kind: SecretStore target: # Kubernetes Secret name # will be mounted to Poenix Pods # .Values.phoenix.auth.name name: phoenix-secret creationPolicy: Owner deletionPolicy: Delete data: # key in the Kubernetes Secret # i.e. the variable name in a Pod - secretKey: PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD remoteRef: key: "/{{ .Values.config.env }}/phoenix/phoenix_default_admin_initial_secret" - secretKey: PHOENIX_ADMIN_SECRET remoteRef: key: "/{{ .Values.config.env }}/phoenix/phoenix_admin_secret" property: password - secretKey: PHOENIX_SECRET remoteRef: key: "/{{ .Values.config.env }}/phoenix/phoenix_secret" - secretKey: PHOENIX_POSTGRES_PASSWORD remoteRef: key: "/{{ .Values.config.env }}/phoenix/phoenix_postgres_credentials" # make it empty - secretKey: PHOENIX_SMTP_PASSWORD remoteRef: key: "/{{ .Values.config.env }}/phoenix/ops_phoenix_smtp_password"
Створюємо Kubernetes Namespace:
$ kk create ns ops-phoenix-ns namespace/ops-phoenix-ns created
Деплоїмо чарт і перевіряємо ресурси – SecretStore:
$ kk get SecretStore phoenix-secret-store NAME AGE STATUS CAPABILITIES READY phoenix-secret-store 14m Valid ReadWrite True
ExternalSecret:
$ kk get externalsecret NAME STORE REFRESH INTERVAL STATUS READY phoenix-external-secret phoenix-secret-store 5m SecretSynced True
Та Kubernetes Secret:
$ kk get secret NAME TYPE DATA AGE phoenix-secret Opaque 1 2m15s
Перевіряємо дані в ньому:
$ kk get secret phoenix-secret -o yaml apiVersion: v1 data: PHOENIX_ADMIN_SECRET: RnB***lY= PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD: P0Z***Tp6 PHOENIX_POSTGRES_PASSWORD: TWo***Uty PHOENIX_SECRET: OVJ***FI= PHOENIX_SMTP_PASSWORD: NXR***VlK kind: Secret ...
Отримуємо реальні значення з base64 -d
:
$ echo NXR***VlK | base64 -d 5tgdKoDr9YYJ
Звіряємо з даними в AWS Secrets Manager.
Підключення до PostgreSQL
В values додаємо параметри для Postgres:
... phoenix-helm: auth: # Kubernetes Secret name name: phoenix-secret # use AWS RDS instead of deploying local postgresql: enabled: false database: postgres: host: db.monitoring.ops.example.co user: ops_phoenix_user db: ops_phoenix_db ...
Деплоїмо, перевіряємо:
Налаштування Ingress
Сам Ingress enabled by default, тому нам треба тільки додати атрибути, через які він “замапиться” на наш загальний AWS Application Load Balancer через анотацію alb.ingress.kubernetes.io/group.name
.
Але і тут є нюанс: в чарті нема можливості задати spec.ingressClassName="alb"
.
Тому робимо трохи deprecated way, теж через annotations
:
... ingress: enabled: true host: phoenix.ops.example.co tls: enabled: true annotations: alb.ingress.kubernetes.io/group.name: ops-1-33-external-alb alb.ingress.kubernetes.io/target-type: ip alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:492***148:certificate/e7145895-9506-4683-a56a-ba6bf98596c5 alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]' alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}' kubernetes.io/ingress.class: alb ...
Ну і власне на цьому все.
Все завелось, все (поки що) працює.
Перший логін робимо з паролем PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD
, далі Phoenix запросить його змінити – задаємо наш із PHOENIX_ADMIN_SECRET
, віддаємо девелоперам на погратись:
Готово.