Scope¶
This runbook builds a complete, self-contained CyVerse deployment on two servers — the shape used for the most recent pilot. It is the narrative companion to the per-component procedures under deployment; every phase below links to the document that carries the full detail.
A deployment of this size supports a working Discovery Environment (DE) with VICE analyses, a single-provider iRODS zone, and one PostgreSQL instance shared by the Data Store and the DE. Scaling out means adding worker nodes and splitting roles onto more hosts; the order of operations does not change.
Placeholders and secrets
Every site-specific value in this document is a placeholder. Substitute your own and keep the real values in a private inventory repository — never in this documentation and never in a public repo.
| Placeholder | Meaning | Example |
|---|---|---|
<BASE_DOMAIN> |
DNS domain the deployment is served under | example.org |
<SITE> |
Short site name used in realm, client, and account names | pilot |
core-1 |
Node hosting HAProxy, PostgreSQL, RabbitMQ, iRODS, k8s control plane, DE services | core-1.<BASE_DOMAIN> |
analysis-1 |
Node hosting VICE analyses | analysis-1.<BASE_DOMAIN> |
<IRODS_ZONE> |
iRODS zone name | pilotZone |
<LDAP_BASE_DN> |
LDAP base DN | dc=example,dc=org |
<DEPLOYMENT_REPO> |
Your private inventory and group_vars repo | <SITE>-deployment |
<GENERATED_SECRET> |
A value you generate per install, never reuse | output of openssl rand -base64 36 |
Generate every password, salt, zone key, and negotiation key at install
time, for example with openssl rand -base64 36 (passwords) or
openssl rand -hex 16 (alphanumeric keys and salts). Store them in the
private inventory, and let Ansible template them into place rather than
typing them into shells where they land in history.
Deployment secrets that Ansible generates for you — the GPG and PEM files used by Terrain, apps, and iplant-groups — belong in the private inventory repository, not in this one.
Node layout¶
Two servers, split by workload rather than by service:
| Node | Runs |
|---|---|
core-1 |
HAProxy, PostgreSQL, RabbitMQ, iRODS provider, Kubernetes control plane, DE services (everything except analyses) |
analysis-1 |
Kubernetes worker dedicated to VICE analyses |
Sizing, per-component reservations, and the dependency graph are in component inventory. The ports that must be open between the two nodes, to administrators, and to the public internet are in network requirements.
Reading the dependency graph from the bottom up gives the order the rest of this document follows:
graph TD
NET[Phase 0: network and firewalls] --> FOUND[Phase 1: HAProxy, PostgreSQL, RabbitMQ]
FOUND --> DB[Phase 2: service databases]
DB --> IRODS[Phase 3: iRODS provider and DE integration]
IRODS --> K8S[Phase 4: Kubernetes and cluster add-ons]
K8S --> CORE[Phase 5: LDAP, Keycloak, Grouper, OpenSearch, NATS, CSI driver]
CORE --> APPS[Phase 6: DE services, VICE, User Portal]
APPS --> POST[Phase 7: bootstrap admin, VICE operator, apps]
Phase 0: network and firewalls¶
Do this first. Every later phase fails in a confusing way if a port is closed.
- Confirm host firewalls on both nodes and any institutional firewall allow the traffic listed in network requirements.
- Confirm
80/tcpand443/tcponcore-1are reachable from everywhere users and VICE clients connect from. - Confirm iRODS (
1247/tcpplus the20000-20199data range) is reachable fromanalysis-1and from any host that will move data in or out. - Confirm outbound HTTPS to your DNS provider's API is allowed, so cert-manager can complete Let's Encrypt DNS-01 challenges. This is optional but strongly recommended; without it, certificate issuance is manual.
- Create the DNS records you will need:
de.<BASE_DOMAIN>,keycloak.<BASE_DOMAIN>,user.<BASE_DOMAIN>,vice.<BASE_DOMAIN>, and a wildcard*.vice.<BASE_DOMAIN>for interactive apps.
Phase 1: foundation services on core-1¶
Full procedures: HAProxy, PostgreSQL, RabbitMQ.
1.1 HAProxy¶
Install HAProxy on core-1. It terminates public HTTPS on 443/tcp and
forwards to the node ports Traefik listens on inside the cluster. Its
configuration depends on the Traefik node ports chosen in Phase 4, so install
the package now and configure it with the haproxy playbook tag later in that
phase.
1.2 PostgreSQL¶
One PostgreSQL instance backs both the iRODS catalog (iCAT) and every DE service database.
- Disable transparent huge pages: append
transparent_hugepage=nevertoGRUB_CMDLINE_LINUX_DEFAULTin/etc/default/grub, runupdate-grub, and reboot. - Set kernel parameters persistently (
/etc/sysctl.d/):vm.nr_hugepages = 0.6 * (memory_available_to_postgres_in_kiB / 2048)andvm.swappiness = 5. - Install
postgresql,postgresql-client, andpython3-psycopg2. - Apply the tuned
postgresql.confsettings from PostgreSQL — they derive from the cores and memory you reserved for the database, so compute them from your own sizing rather than copying literals. - Restart PostgreSQL, then follow the iRODS project's instructions for preparing PostgreSQL for iRODS.1
- Create a role for the DE with
SELECTon all tables in the iCAT database, with a generated password.
standard_conforming_strings
The DE database expects standard_conforming_strings = off. If it is left
on, some migrations do not apply; see
troubleshooting.
1.3 RabbitMQ¶
- Install
rabbitmq-server, enable therabbitmq_managementplugin, restart. - Create an administrator account with a generated password, grant it full
configure/write/read permissions, and tag it
administrator. - Delete the default
guestaccount. - Create the vhost
/data-storeand grant the administrator full configure/write/read on it. - Create a separate account for iRODS with a generated password and full
configure/write/read on
/data-store. - Create a
topicexchange namedirodson/data-store.
The DE's own RabbitMQ objects are created later, in Phase 6, by the
rabbitmq_configure.yml playbook.
Phase 2: service databases¶
Full procedures: databases.
The iCAT database itself is created by the iRODS installer in Phase 3. The DE's
databases are created and migrated by Ansible in Phase 4 (the
setup-databases and update-databases tags), so at this point you only need
to make PostgreSQL reachable:
- Set a password for the
postgresrole. - Edit
pg_hba.confto allow connections from your admin hosts and from both nodes, using methodscram-sha-256. - Restart PostgreSQL.
The pod network cannot be allowed yet — its CIDR does not exist until the
cluster is created. Phase 4 comes back to pg_hba.conf for that.
Phase 3: iRODS provider and DE integration¶
Full procedures: iRODS provider, DE integration.
3.1 Logging first¶
Create the rsyslog configuration before installing iRODS, so the first
server start is captured. Write /etc/rsyslog.d/00-irods.conf:
$FileCreateMode 0644
$DirCreateMode 0755
$Umask 0000
$template irods_format,"%msg%\n"
:programname,startswith,"irodsServer" /var/log/irods/irods.log;irods_format
& stop
:programname,startswith,"irodsDelayServer" /var/log/irods/irods.log;irods_format
& stop
:programname,startswith,"irodsAgent" /var/log/irods/irods.log;irods_format
& stop
Then add log rotation in /etc/logrotate.d/irods:
/var/log/irods/irods.log {
weekly
rotate 26
copytruncate
delaycompress
compress
dateext
notifempty
missingok
su root root
}
3.2 Install the iRODS 4.3.3 catalog provider¶
- Set TCP keepalive to 120 seconds with
sysctl. - Install
python-is-python3andpython3-pika. - Add the iRODS apt repository2 and pin
irods-*to4.3.3so an unattended upgrade cannot move the catalog provider. - Run the iRODS setup script1 with the answers in
iRODS provider —
service account
irods, roleprovider, ODBC driverPostgreSQL Unicode, catalog onlocalhost:5432, zone<IRODS_ZONE>, port1247, data port range20000-20199.
Three answers deserve care:
- Password salt — a generated alphanumeric string. Never leave it empty; an empty salt makes stored passwords recoverable.
- Zone key and negotiation key — generated alphanumeric strings; the zone key must be under 40 characters.
- Default resource name — anything but
demoResc, with its vault directory at the root of the filesystem that will hold the data.
3.3 Install CyVerse policy¶
From the Data Store collection
branch that matches your site,3 as the irods service account:
- Copy
playbooks/files/irods/var/lib/irods/msiExecCmd_bin/*into/var/lib/irods/msiExecCmd_bin/and make them executable. - Render
playbooks/templates/irods/etc/irods/cyverse-env.re.j2to/etc/irods/cyverse-env.re, settingcyverse_RE_HOSTto the FQDN ofcore-1andcyverse_ZONEto<IRODS_ZONE>. - Copy
playbooks/files/etc/irods/*into/etc/irods/.
Then edit /etc/irods/server_config.json:
{
"advanced_settings": {
"number_of_concurrent_delay_rule_executors": 12
},
"environment_variables": {
"IRODS_AMQP_URI": "amqp://<IRODS_AMQP_USER>:<GENERATED_SECRET>@localhost:5672/%2Fdata-store"
},
"plugin_configuration": {
"rule_engines": [
{ "re_rulebase_set": ["cve", "cyverse_core", "core"] }
]
}
}
The rule base order is significant: cve overrides cyverse_core, which
overrides core.
Enable the service so it starts at boot, and start it.
3.4 Runtime initialization¶
As the irods service account, with <IRODS_ZONE> substituted throughout:
- Create the
rodsadmingroup and addrodsto it. Remove the collections/<IRODS_ZONE>/home/rodsadminand/<IRODS_ZONE>/trash/home/rodsadminthat group creation leaves behind. - Remove
/<IRODS_ZONE>/trash/home/public. - Ensure the predefined collections
/<IRODS_ZONE>,/<IRODS_ZONE>/home,/<IRODS_ZONE>/home/public,/<IRODS_ZONE>/home/rods,/<IRODS_ZONE>/trash,/<IRODS_ZONE>/trash/home, and/<IRODS_ZONE>/trash/home/rodsall carry time-based (version 1) UUIDs. - Grant
rodsadminwriteon/<IRODS_ZONE>,/<IRODS_ZONE>/home,/<IRODS_ZONE>/trash, and/<IRODS_ZONE>/trash/home; grant itownon/<IRODS_ZONE>/home/rodsand/<IRODS_ZONE>/trash/home/rods. - Create the
anonymousrodsuserwithout a password and grant itreadon/<IRODS_ZONE>and/<IRODS_ZONE>/home. This is what makes public data public.
3.5 Prepare iRODS for the DE¶
- Install the DE's specific queries from
playbooks/files/irods/specific-queriesin the Data Store collection. Each file name is the query alias and the file contents are the query:
iadmin asq "$(cat IPCCountCollectionsUnderPath.sql)" IPCCountCollectionsUnderPath
- Create the DE's iRODS admin account:
iadmin mkuser de-irods rodsadmin
iadmin moduser de-irods password '<GENERATED_SECRET>'
iadmin atg rodsadmin de-irods
Phase 4: Kubernetes and cluster add-ons¶
Full procedures: cluster, resources, cert-manager, ingress, storage, Harbor, Argo.
4.1 Inventory and group variables¶
Start from the example inventory in ansible/example/inventory in the
deployment playbook repository and copy it into your private
<DEPLOYMENT_REPO>:
| Inventory group | Contents for a two-node pilot |
|---|---|
01_condor |
empty — HTCondor is not used in this deployment |
02_dbms |
the database host; both groups may name the same host |
03_gocd |
empty — no GoCD in this deployment |
04_haproxy |
core-1 |
05_k8s |
core-1 in k8s_api_proxy, k8s_controllers, and k8s_de_workers; analysis-1 in k8s_vice_workers |
group_vars/all.yml in the example inventory is organized in phases, because
some variables can only be filled in from values produced by earlier phases
(Keycloak client secrets, for example). Fill in phases 1 through 4 now.
4.2 Databases, node prep, HAProxy¶
Run from the ansible directory of the deployment playbook repository, with
-i pointing at your private inventory:
ansible-playbook -i /path/to/inventory --tags setup-databases,update-databases kubernetes.yml
ansible-playbook -i /path/to/inventory --tags prep-nodes kubernetes.yml
ansible-playbook -i /path/to/inventory --tags haproxy kubernetes.yml
The PostgreSQL installation role is skipped here on purpose: the Data Store's DBMS from Phase 1 is being reused.
4.3 Create the k0s cluster¶
Write a k0sctl.yaml listing both hosts and the cluster settings — see
cluster for a sanitized example — then:
export K0S_SSH_USER=<SSH_USER>
export K0S_SSH_KEY_PATH=/path/to/private-key
export KUBECONFIG="$HOME/.kube/config"
mkdir -p "$(dirname "$KUBECONFIG")"
k0sctl apply --config /path/to/k0sctl.yaml
Because core-1 is both control plane and DE worker, remove the control-plane
taint if k0s applied one. On the control node:
k0s kubectl taint node core-1 node-role.kubernetes.io/control-plane:NoSchedule-
If the node was never tainted, the command reports that there is nothing to remove; that is the expected outcome, not an error to chase.
4.4 Cluster add-ons, in order¶
ansible-playbook -i /path/to/inventory --tags cert-manager kubernetes.yml
ansible-playbook -i /path/to/inventory --tags cert-issuers kubernetes.yml
ansible-playbook -i /path/to/inventory --tags argo kubernetes.yml
ansible-playbook -i /path/to/inventory argo_resources.yml
ansible-playbook -i /path/to/inventory --tags ingress-nginx kubernetes.yml
ansible-playbook -i /path/to/inventory --tags traefik kubernetes.yml
ansible-playbook -i /path/to/inventory --tags longhorn kubernetes.yml
ansible-playbook -i /path/to/inventory --tags harbor kubernetes.yml
ingress-nginx is transitional
Traefik is the ingress the deployment is standardizing on. ingress-nginx is still installed for VICE ingresses and will be removed once that migration completes. See ingress.
4.5 Let the pods reach PostgreSQL¶
The cluster now has a pod network, so add it to pg_hba.conf:
kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' && echo
For each distinct CIDR, add a line to pg_hba.conf (in a Debian packaged
PostgreSQL 16 install, /etc/postgresql/16/main/pg_hba.conf):
host all all <pod-cidr> scram-sha-256
Restart PostgreSQL afterwards. In a single-node-per-role pilot the CIDRs are usually identical, but check rather than assume.
Phase 5: core services¶
Full procedures: OpenLDAP, Keycloak, Grouper, OpenSearch, NATS, iRODS CSI driver.
5.1 DE prerequisites and directory services¶
ansible-playbook -i /path/to/inventory --tags de-reqs kubernetes.yml
ansible-playbook -i /path/to/inventory --tags openldap-docker kubernetes.yml
5.2 Keycloak¶
ansible-playbook -i /path/to/inventory --tags keycloak kubernetes.yml
Add a DNS record for keycloak.<BASE_DOMAIN> if you have not already. An
/etc/hosts entry will get you through the next few steps, but it is not a
deployment.
The realm, LDAP federation, mappers, roles, and eight OAuth clients are then
configured in the Keycloak UI. That configuration is long, exact, and produces
the client secrets that later phases need in group_vars/all.yml, so it lives
in its own document: Keycloak. Complete it
before continuing.
5.3 Service signing keys¶
From the same directory you run ansible-playbook from:
./scripts/generate-secrets.sh /path/to/inventory
This writes the GPG and PEM files Terrain, apps, and iplant-groups expect, and
prints a YAML snippet to add to group_vars/all.yml. Commit the generated key
material to your private inventory repository only.
5.4 Configuration, ingress, networking, messaging¶
ansible-playbook -i /path/to/inventory \
--tags=configure-services,ingress,networking,nats kubernetes.yml
If NATS has to be reinstalled, Helm may still hold a release record; remove it
with helm -n prod uninstall nats before re-running.
5.5 Scheduling, caching, search, groups, storage¶
ansible-playbook -i /path/to/inventory \
--tags=feature-discovery,image-cache,grouper kubernetes.yml
ansible-playbook -i /path/to/inventory --tags=opensearch kubernetes.yml
ansible-playbook -i /path/to/inventory --tags=irods-csi-driver kubernetes.yml
Node feature discovery labels nodes by capability (GPUs, for instance) so apps that need specific hardware land on the right worker. Image caching pre-pulls frequently used VICE images so interactive apps start promptly.
5.6 DE messaging objects¶
ansible-playbook -i /path/to/inventory rabbitmq_configure.yml
Phase 6: applications¶
Full procedures: Discovery Environment, VICE, User Portal.
ansible-playbook -i /path/to/inventory --tags=deploy-all-services kubernetes.yml
This deploys the full DE service set, the User Portal, and the VICE backend.
Watch for pods that never reach Running:
kubectl get pods -A | grep -Ev 'Running|Completed'
Phase 7: bootstrap and first analyses¶
Full procedures: bootstrap, verification, troubleshooting.
7.1 Bootstrap portal administrator¶
ansible-playbook -i /path/to/inventory bootstrap_portal_admin.yml
Run this from a host that can both kubectl port-forward into the cluster and
reach the portal database with psql. The playbook port-forwards to OpenLDAP
and portal-conductor and connects to the portal database directly, so a
pg_hba.conf rule for that host has to exist. Getting this far means the
port-forwarding half already works; the database half is the one that bites.
This creates the <SITE>-bootstrap user, the account you use for everything
below.
7.2 Register a VICE operator¶
Interactive analyses will not launch until an operator is registered.
- Log in to
https://de.<BASE_DOMAIN>as<SITE>-bootstrap. - Open the left navigation from the hamburger menu, then Admin → VICE → Operators.
- Click +New and set:
- Name:
prod - URL:
http://vice-operator.vice-apps:10000— plain HTTP is correct here; the connection never leaves the cluster network. - Public base URL:
https://vice.<BASE_DOMAIN> - Priority:
0 - Click Register operator.
7.3 Import a starting set of apps¶
The scripts/appei directory of the deployment repository exports and imports
apps and tools between DEs, and manages its dependencies with
uv. From that directory:
uv run appei login --server de.<BASE_DOMAIN> --username <SITE>-bootstrap
uv run appei import --server de.<BASE_DOMAIN> -i de-word-count.json --publish
uv run appei import --server de.<BASE_DOMAIN> -i cloudshell.json --featured
uv run appei import --server de.<BASE_DOMAIN> -i portal-delete-user.json
The result, as <SITE>-bootstrap:
| App | Visibility |
|---|---|
| DE Word Count | public, not featured |
| Cloud Shell | featured, and therefore public |
| portal-delete-user | private to the bootstrap account |
portal-delete-user needs a configuration file attached before it will run;
bootstrap covers that,
and troubleshooting covers repairing
the import if it lands with the wrong visibility.
7.4 Verify¶
Work through verification: sign in through Keycloak, browse the Data Store, run the DE Word Count app on a small input, and launch Cloud Shell to confirm VICE end to end.