AWS Installation (Helm)
This guide walks through deploying JuliaHub as a self-managed installation on Amazon Web Services using Helm. By the end, you will have a running JuliaHub platform on an Amazon Elastic Kubernetes Service (EKS) cluster with Amazon EFS storage and an external PostgreSQL database.
There are two ways to provision the underlying AWS infrastructure. You can use the public JuliaHub Terraform modules, which create everything the chart expects, or you can bring an existing EKS cluster and provision the pieces yourself with whatever tooling you prefer. Both paths are described in Infrastructure Setup, and they converge on the same Helm values.
Prerequisites
Before starting, review the Common Prerequisites shared by all installation methods — in particular the outbound network access requirements. In addition, ensure you have the following:
Hostname
Decide on a hostname for your JuliaHub installation (e.g. juliahub.example.com). You will need to create DNS records after the installation is complete, so the hostname does not need to resolve yet, but it must be decided now as it is baked into the TLS certificate and Helm values.
TLS Certificate
The TLS certificate must cover the following domains:
<hostname>— the main platform URL*.apps.<hostname>— JuliaHub-managed application routingdocs.<hostname>— generated Julia package documentation
A wildcard certificate for *.<hostname> plus the bare <hostname> is the simplest option.
Recommended: offload TLS to an ALB with an ACM certificate. On AWS the common approach is to terminate TLS at an Application Load Balancer using a certificate from AWS Certificate Manager (ACM). You request (or import) the certificate in ACM, reference its ARN on the ALB Ingress, and set offloadTLS: true so the chart does not manage certificates itself. This keeps certificate issuance and renewal in ACM rather than in Helm values. This is the default path in this guide — see Exposing the platform.
Alternative: supply the certificate to the chart. If you are not using an ALB (for example, you expose the platform through a LoadBalancer Service and terminate TLS in the cluster), obtain the certificate and private key in PEM format and provide them in one of two ways:
- Inline in Helm values: Save the full certificate chain as
fullchain.pemand the private key asprivkey.pem, then settlsFullchainPemandtlsPrivkeyPemin your values file. - As a Kubernetes TLS Secret: Create a
kubernetes.io/tlsSecret in the target namespace and setcertsSecretNamein your values file. When using this method,tlsFullchainPemandtlsPrivkeyPemdo not need to be set.
Replicated License
JuliaHub self-managed installations are distributed through Replicated. Contact your JuliaHub sales representative to obtain:
- A license ID (used as the Helm registry password)
- A license email (used as the Helm registry username)
Initial Administrator
Decide the email address for the initial administrator account, set through settings.initialUsers in your Helm values. This account exists so that somebody can sign in and configure single sign-on, which is the preferred way to authenticate users; it needs a password only if you are not configuring a provider during the installation. See Common Prerequisites.
CLI Tools
Install the following on your workstation:
| Tool | Minimum Version | Purpose |
|---|---|---|
AWS CLI (aws) | 2.13 | AWS resource management |
eksctl | 0.170 | EKS cluster provisioning (optional, convenience) |
| Helm | 3.0 | Kubernetes package manager |
| kubectl | 1.28 | Kubernetes CLI |
You will also need the AWS Load Balancer Controller installed in the cluster for the recommended ALB exposure path (see Exposing the platform).
Log in to AWS:
aws configure # or export AWS_PROFILE / use SSO
aws sts get-caller-identity # confirm the right account and regionInfrastructure Setup
Choose one of the following paths depending on whether you are starting from scratch or have an existing EKS cluster.
Option A is not all-or-nothing. Each piece it provisions can be turned off individually, so you can bring what you already have and let Terraform create the rest:
| Bring your own | Set |
|---|---|
| VPC and subnets | vpc_id, private_subnet_ids, public_subnet_ids |
| PostgreSQL | create_rds = false |
| Config directory filesystem | create_efs_config_directory = false |
| Userdata filesystem | create_efs_userdata_directory = false |
| Datasets bucket, log groups and job IAM | create_compute = false |
| Karpenter IAM | create_karpenter_iam = false |
| Load balancer IAM | create_alb_controller_iam = false |
An existing VPC is the common case and is covered in Deploying into an existing VPC below — note the subnet tags it calls out.
The EKS cluster is the one thing the root module always creates. If you already run a cluster, call the submodules directly instead of the root module and use them for what the platform adds around it — EFS, RDS, the datasets bucket, the certificate and the IAM roles. See Using the modules individually for a worked example, and the note there on the wiring the root module would otherwise do for you.
If you use no Terraform at all, the IAM policies the modules attach are still usable on their own — see IAM Policies.
Option A: Using JuliaHub Terraform Modules
JuliaHub publishes public Terraform modules that provision the AWS infrastructure the platform needs: a VPC with public and private subnets, an EKS cluster with the CSI drivers installed, RDS for PostgreSQL, two EFS filesystems with access points, the datasets S3 bucket, an ACM certificate, and the IAM roles the platform and cluster controllers assume.
Everything except the cluster itself is optional, so this path still applies if you already have a VPC, a database or storage — see the tip above.
1. Clone the modules:
git clone https://github.com/JuliaComputing/platform-public-terraform-modules.git
cd platform-public-terraform-modules/awsThe AWS module README is the reference for what follows: every variable, what each submodule creates, and the outputs this guide feeds into your Helm values.
2. Create your configuration:
cp terraform.tfvars.example terraform.tfvarsEdit terraform.tfvars to set your values. At minimum, update:
region = "us-east-1"
cluster_name = "juliahub"
# Availability zones must be in your chosen region. EKS requires at least two.
availability_zones = ["us-east-1a", "us-east-1c"]
# Identifies the install. Names the IAM roles, derives the S3 bucket names, and
# is the default CORS origin for direct dataset uploads, so it must be the
# hostname users load the platform from.
platform_hostname = "juliahub.example.com"
# Restrict this to your own egress ranges rather than leaving it open.
endpoint_public_access_cidrs = ["0.0.0.0/0"]See the module README for the full set of configurable variables, the opt-out flags, and how each output maps to a Helm value.
If you already have a VPC, pass it in and the modules skip creating one — everything else (EKS, EFS, RDS, the datasets bucket, the IAM roles) is still provisioned, in the subnets you name:
vpc_id = "vpc-0123456789abcdef0"
private_subnet_ids = ["subnet-0aaa...", "subnet-0bbb..."]
public_subnet_ids = ["subnet-0ccc...", "subnet-0ddd..."]The VPC-shaping settings (vpc_cidr, the subnet CIDR lists, availability_zones, the endpoint flags) are then ignored. Private subnets still need outbound internet access, or VPC endpoints for ECR, S3 and CloudWatch Logs, or nodes cannot pull images.
Your subnets must carry the discovery tags. The AWS Load Balancer Controller and Karpenter do not take a list of subnets — they find them by tag:
| Tag | On | If missing |
|---|---|---|
kubernetes.io/role/elb | public subnets | The platform Ingress never gets an ALB |
kubernetes.io/role/internal-elb | private subnets | No internal load balancer is placed |
karpenter.sh/discovery = <cluster_name> | private subnets | No job nodes are ever provisioned |
None of these fail at apply time. The cluster comes up and the platform reports healthy, then no load balancer appears or jobs sit pending forever. Set tag_existing_subnets = true to have the modules apply the tags — off by default, since Terraform then manages tags on subnets it did not create and terraform destroy removes them again — or apply them yourself, in which case the modules check for them at plan time and name any that are missing.
platform_hostname is the CORS origin for direct dataset uploads, so it has to be the real hostname. It also seeds the names of the generated buckets, IAM roles and log groups, and those have hard limits — 63 characters for an S3 bucket, 64 for an IAM role. A long hostname plus the derived suffixes exceeds them and fails partway through apply, after the VPC and cluster already exist.
Set resource_name_prefix when that is a risk. It shortens the resource names without touching the CORS origin:
platform_hostname = "juliahub.long.subdomain.example.com"
resource_name_prefix = "juliahub"3. Apply the infrastructure:
terraform init
terraform applyThis takes approximately 20–25 minutes. Once complete, Terraform outputs the connection details needed for later steps.
4. Connect to the EKS cluster:
aws eks update-kubeconfig \
--region "$(terraform output -raw region)" \
--name "$(terraform output -raw cluster_name)"5. Install the cluster controllers:
Neither Karpenter nor the AWS Load Balancer Controller is available as an EKS managed add-on, so the modules create only their IAM roles and you install the charts yourself. Both need tolerations for the taint on the cluster's initial node group, which the modules emit ready-formed:
tolerations=$(terraform output -raw critical_node_tolerations_helm_set)
helm upgrade --install aws-load-balancer-controller aws-load-balancer-controller \
--repo https://aws.github.io/eks-charts \
--namespace kube-system \
--set "clusterName=$(terraform output -raw cluster_name)" \
--set "region=$(terraform output -raw region)" \
--set "vpcId=$(terraform output -raw vpc_id)" \
--set serviceAccount.create=true \
--set serviceAccount.name=aws-load-balancer-controller \
--set-string "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=$(terraform output -raw alb_controller_role_arn)" \
${tolerations} \
--wait
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \
--namespace kube-system \
--set "settings.clusterName=$(terraform output -raw cluster_name)" \
--set "settings.interruptionQueue=$(terraform output -raw karpenter_interruption_queue_name)" \
--set "settings.vmMemoryOverheadPercent=0.04" \
--set-string "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=$(terraform output -raw karpenter_controller_role_arn)" \
${tolerations} \
--waitLeft unset, the controller discovers them from EC2 instance metadata, which the nodes block by design (IMDS hop limit of 1). It then crash-loops with failed to get VPC ID ... context deadline exceeded. Pass both explicitly rather than raising the hop limit, which would expose node credentials to every pod.
6. Create the Karpenter node pools:
Karpenter provisions nothing until you give it an EC2NodeClass and at least one NodePool. The selectors below match the discovery tags the modules apply to the private subnets and the cluster security group. Adjust the instance types to suit your workloads:
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: juliahub
spec:
amiFamily: Bottlerocket
amiSelectorTerms:
- alias: bottlerocket@latest
role: <karpenter_node_role_name>
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: <cluster_name>
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: <cluster_name>
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: services
spec:
template:
metadata:
labels:
juliarun/node-class: services
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: juliahub
requirements:
- key: "karpenter.sh/capacity-type"
operator: In
values: ["on-demand"]
limits:
cpu: "32"Add a second NodePool for job capacity. Keep it separate from the platform's own nodes so a large job cannot displace the services that schedule it:
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: jobs
spec:
template:
metadata:
labels:
juliarun/schedule: "yes"
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: juliahub
requirements:
# One instance type per default node spec, from r62 (2 vCPU / 16 GiB)
# up to r632 (32 vCPU / 256 GiB). A job asking for a spec with no
# matching instance type here stays Pending: Karpenter has nothing it
# is allowed to launch. Trim the list if you offer fewer specs.
- key: "node.kubernetes.io/instance-type"
operator: In
values:
- r6i.large # r62 2 vCPU / 16 GiB
- r6i.xlarge # r64 4 vCPU / 32 GiB
- r6i.2xlarge # r68 8 vCPU / 64 GiB
- r6i.4xlarge # r616 16 vCPU / 128 GiB
- r6i.8xlarge # r632 32 vCPU / 256 GiB
- key: "karpenter.sh/capacity-type"
operator: In
values: ["on-demand"]
# Total CPU across every node in this pool, not per node. It has to be at
# least the largest single spec you offer, and in practice several times that
# if jobs run concurrently — jobs queue once the pool is at its limit.
limits:
cpu: "256"
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30sGPU jobs need their own pool: the p31 spec (8 vCPU / 61 GiB / 1 GPU) has no match in the r6i family, so add a NodePool with a GPU instance type such as p3.2xlarge and the NVIDIA device plugin installed. See Node Provisioning for Jobs.
7. Collect the values for the chart:
Terraform outputs everything the chart needs:
terraform outputThe mapping from output to Helm value is given in Create Your Values File.
Continue to Install JuliaHub.
Option B: Existing EKS Cluster
If you already have an EKS cluster, or prefer to provision with your own tooling, create the following resources yourself. The rightmost column shows which Helm value each one maps to; the mappings are collected into a single values file in Create Your Values File.
Before provisioning, review AWS's EKS VPC and subnet requirements and the VPC and subnet considerations in the EKS Best Practices Guide. In particular, plan for enough IP address space for pods and jobs, and — if you use the recommended ALB path — tag the load balancer subnets (kubernetes.io/role/elb on public subnets or kubernetes.io/role/internal-elb on private subnets) so the AWS Load Balancer Controller can discover them.
| Resource | Why it is needed | Maps to Helm value |
|---|---|---|
| VPC + subnets | Network for the EKS cluster and its load balancer. Use private subnets for nodes and public (or internal) subnets for the load balancer, across at least two Availability Zones. See EKS VPC and subnet requirements. | (infrastructure only — not a chart value) |
| EKS cluster | Runs the JuliaHub platform. Kubernetes 1.28+. | Target cluster for helm install |
| Managed node group(s) | Baseline worker nodes for the platform's own pods. | (infrastructure only) |
| Node autoscaler(compute only) | Brings worker nodes up and down on demand for jobs. Karpenter is recommended (JuliaHub uses it); Cluster Autoscaler is a lighter alternative. See Node Provisioning for Jobs. | (cluster add-on) |
| EFS filesystem + access point(s) | ReadWriteMany storage for the platform configuration directory (and, if compute is enabled, job userdata). | configDirectory.efs.*, compute.userdataDirectory.efs.* |
| AWS EFS CSI driver | Kubernetes driver (efs.csi.aws.com) that mounts the EFS filesystem. The chart references this driver but does not install it. | (cluster add-on) |
| PostgreSQL database | Primary relational store. Amazon RDS for PostgreSQL (or Aurora PostgreSQL), reachable from the cluster's VPC. | postgres.external.* |
| S3 bucket(compute only) | Object storage for dataset and job results. Only needed when compute is enabled. | compute.storage.aws.bucketName |
| IAM role for compute storage(compute only) | Grants JuliaHub jobs access to the S3 bucket. | compute.storage.aws.storageRoleArn |
| IAM role for the cloud host(compute only) | Role JuliaHub assumes to launch compute workloads. | compute.cloudhost.aws.roleArn |
| ACM certificate | TLS certificate for the platform hostname(s), managed by AWS Certificate Manager and attached to the ALB. This is the recommended way to handle TLS on AWS. | offloadTLS: true + ALB Ingress annotation |
| AWS Load Balancer Controller | Provisions the ALB from the Ingress resource and applies the ACM certificate and alb.ingress.kubernetes.io/* annotations. Required for the recommended ALB path. | (cluster add-on) |
| Application Load Balancer | Exposes the platform to users and terminates TLS with the ACM certificate. Created automatically by the AWS Load Balancer Controller from the Ingress. | websrvr.ingress.* |
| Route 53 (or your DNS provider) | Resolves the platform hostname(s) to the load balancer. Created after install. | See DNS Configuration |
IAM Policies
Provisioning IAM by hand is the fiddliest part of Option B, and the policies are easy to get wrong in ways that only surface at runtime — a job that cannot write its results, or an EFS mount that is refused.
You do not have to derive them. The Terraform modules keep the policies they attach as standalone template files, so they can be read and adapted whatever tooling you use:
| Policy | Grants |
|---|---|
platform.json.tftpl | Assume the compute roles, read the image registry, manage log groups, mount EFS |
datasets.json.tftpl | Read and write the platform prefixes of the datasets bucket |
jobs.json.tftpl | Write job and audit log streams, manage job secrets |
job-outputs.json.tftpl | Multipart upload into the results prefix |
logging.json.tftpl | Write log streams, read log archives |
These are Terraform template files rather than plain JSON so that every substitution is explicit: each ${...} marks a value from your own account — bucket ARNs, log group ARNs, your account ID. A ${jsonencode(...)} placeholder expects a JSON array, a bare ${...} a string. Replace them and the result is a valid IAM policy document.
The Provisioning IAM without terraform section of the module README lists every placeholder and its meaning.
Two roles in the table above map onto these policies: the compute storage role (compute.storage.aws.storageRoleArn) carries the datasets policy, and the cloud host role (compute.cloudhost.aws.roleArn) carries the jobs policy. The trust policies are not templated — they depend on your cluster's OIDC provider — but iam.tf shows the shape, including the system:serviceaccount:<namespace>:<name> subject conditions the IRSA role needs.
EFS Access Points
The chart mounts EFS through the efs.csi.aws.com CSI driver and creates the PersistentVolume itself from the values you supply — you do not create a Kubernetes StorageClass for EFS. You do, however, need to provision the EFS filesystem and (recommended) an EFS access point yourself:
- The config directory requires one EFS filesystem. An access point is optional; when set, the PV volume handle becomes
<filesystemId>::<accessPointId>. - The compute userdata directory (only when compute is enabled) requires an access point. The mount roots at the access point's directory, and JuliaHub builds per-job volumes from the filesystem ID and access point ID.
Ensure the EFS filesystem has mount targets in the subnets your node group runs in, and that its security group allows NFS (TCP 2049) from the node security group.
Install JuliaHub
Connect to the EKS Cluster
If you followed Option A you are already connected; skip to Log in to the Replicated Registry.
aws eks update-kubeconfig --name <cluster-name> --region <region>
kubectl get nodes # confirm connectivityInstall the AWS EFS CSI Driver
The Terraform modules install the EFS and EBS CSI drivers as EKS add-ons, with their IRSA roles. Skip this step if you used Option A.
The chart mounts EFS via efs.csi.aws.com but does not install the driver. Install it as an EKS add-on (or via its Helm chart) if it is not already present:
aws eks create-addon \
--cluster-name <cluster-name> \
--addon-name aws-efs-csi-driver \
--region <region>See the AWS EFS CSI driver documentation for IAM setup (an IRSA role for the driver's service account) and prerequisites.
Log in to the Replicated Registry
helm registry login registry.replicated.com \
--username <license-email> \
--password <license-id>View the Full Values Reference
To see all configurable Helm values and their defaults:
helm show values oci://registry.replicated.com/juliahub/production/juliahub-platformCreate Your Values File
Create a myvalues.yaml with the minimal configuration for AWS. Fill in the identifiers of the resources you provisioned above.
The complete file is below — copy it, replace every <...> placeholder, and you have a working values file. The sections that follow explain each block and the choices behind it; you do not need to read them to get an installation up, only to change what it does.
First create the database Secret the file refers to, so it exists before you install. All nine keys are required; the URLENCODED_* variants are the same user and password percent-encoded for use in a connection URI. See Database Credentials for the details.
kubectl --namespace <namespace> create secret generic rds-postgresql \
--from-literal=POSTGRES_HOST="<rds-endpoint>" \
--from-literal=POSTGRES_PORT=5432 \
--from-literal=POSTGRES_DB="<postgres-database>" \
--from-literal=POSTGRES_USER="<postgres-username>" \
--from-literal=POSTGRES_PASSWORD="<postgres-password>" \
--from-literal=URLENCODED_POSTGRES_USER="<url-encoded-username>" \
--from-literal=URLENCODED_POSTGRES_PASSWORD="<url-encoded-password>" \
--from-literal=POSTGRES_REQUIRE_SSL=true \
--from-literal=GRAPHILE_MIGRATE_POSTGRES_REQUIRE_SSL=1# ---------------------------------------------------------------- platform
hostname: '<your-hostname>'
# Terminate TLS at the ALB with an ACM certificate (recommended). If you are
# NOT offloading TLS, set this false and supply tlsFullchainPem /
# tlsPrivkeyPem or certsSecretName instead. See "Exposing the Platform".
offloadTLS: true
# ------------------------------------------------- platform config on EFS
# The chart creates the PV/PVC; you supply the filesystem. Requires the
# efs.csi.aws.com driver in the cluster.
configDirectory:
type: "efs"
efs:
filesystemId: "<fs-xxxxxxxxxxxxxxxxx>"
accessPointId: "<fsap-xxxxxxxxxxxxxxxxx>" # optional for the config dir
useIAM: true # must match the EFS policy
# ------------------------------------------------------ external postgres
# Credentials come from a Secret you create before installing -- see
# "Database Credentials" for the command. Keeping them out of this file means
# the password is not sitting on disk in plaintext.
postgres:
type: external
external:
existingSecretName: "rds-postgresql"
# ----------------------------------------------------------------- compute
# Whether compute runs at all is set by your Replicated license, not here.
compute:
# Per-job persistent storage. accessPointId is REQUIRED for userdata.
userdataDirectory:
type: "efs"
efs:
filesystemId: "<fs-xxxxxxxxxxxxxxxxx>"
accessPointId: "<fsap-xxxxxxxxxxxxxxxxx>"
useIAM: true
storage:
aws:
bucketName: "<datasets-bucket>"
storageRoleArn: "arn:aws:iam::<account-id>:role/<datasets-role>"
cloudhost:
aws:
region: "<region>"
roleArn: "arn:aws:iam::<account-id>:role/<jobs-role>"
maxSessionDuration: 18000
# Where the platform reads job logs back from. Use the log group the
# terraform module created; see Job Logs below for why this must not be
# left to the composed default.
joblogs:
enabled: true
groupName: "<cluster>-<install>-job-logs"
# ---------------------------------------------------------------- identity
# IRSA: applied to the platform and juliarun job ServiceAccounts.
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::<account-id>:role/<juliahub-platform-role>"
# ------------------------------------------------------------ job logs out
# Ships job container logs to CloudWatch. logGroupName must match
# compute.joblogs.groupName above.
logging-fluentbit:
enabled: true
region: "<region>"
logGroupName: "<cluster>-<install>-job-logs"
serviceAccountRoleArn: "arn:aws:iam::<account-id>:role/<platform-role>"
# ------------------------------------------------------- sysimage builds
# CSI driver that sysimage-build jobs mount their bundle through.
shared-bundle-csi-driver:
enabled: true
# ------------------------------------------------------------- ingress
# ALB Ingress with an ACM certificate, matching offloadTLS: true above.
websrvr:
external:
enabled: false
ingress:
enabled: true
className: "alb"
annotations:
alb.ingress.kubernetes.io/scheme: "internet-facing"
alb.ingress.kubernetes.io/target-type: "ip"
alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:<region>:<account-id>:certificate/<id>"
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'If you used the Terraform modules, every identifier below is a Terraform output. Read them with terraform output -raw <name>:
| Helm value | Terraform output |
|---|---|
configDirectory.efs.filesystemId | config_directory_efs_filesystem_id |
configDirectory.efs.accessPointId | config_directory_efs_access_point_id |
compute.userdataDirectory.efs.filesystemId | userdata_directory_efs_filesystem_id |
compute.userdataDirectory.efs.accessPointId | userdata_directory_efs_access_point_id |
postgres.external.host | postgres_host |
postgres.external.port | postgres_port |
postgres.external.username | postgres_username |
postgres.external.password | postgres_password |
compute.storage.aws.bucketName | datasets_bucket_name |
compute.storage.aws.storageRoleArn | datasets_role_arn |
compute.cloudhost.aws.roleArn | jobs_role_arn |
compute.cloudhost.aws.maxSessionDuration | cloudhost_max_session_duration |
compute.cloudhost.aws.region | region |
serviceAccount.annotations role ARN | compute_service_account_role_arn |
compute.joblogs.groupName | job_log_group_name |
logging-fluentbit.logGroupName | job_log_group_name |
logging-fluentbit.serviceAccountRoleArn | compute_service_account_role_arn |
logging-fluentbit.region | region |
configDirectory.efs.useIAM | efs_mounts_require_iam |
compute.userdataDirectory.efs.useIAM | efs_mounts_require_iam |
ALB Ingress certificate-arn annotation | alb_ingress_certificate_arn |
The database password is sensitive. Rather than putting it in myvalues.yaml, which sits on disk in plaintext, create a Secret and set postgres.external.existingSecretName instead — see Database Credentials.
postgres.external.requiresSSL defaults to true. Amazon RDS presents a TLS certificate by default, so leave this enabled unless you have specifically disabled SSL on the database.
Database Credentials
The kubectl create secret command is above, before the values file that refers to it. postgres.external.existingSecretName points the chart at it, and the chart then skips creating its own db-secrets and reads yours instead.
Doing it this way keeps the database password out of myvalues.yaml, which otherwise sits on disk in plaintext and is easy to copy into a ticket or a chat window by accident. The chart does accept username/password/database/host inline instead, but prefer the Secret.
All nine keys are required. The URLENCODED_* variants are the same values percent-encoded for use in a connection URI — if your password contains characters such as /, +, @ or a space, encode it (for example with jq -rn --arg v "$password" '$v|@uri') rather than repeating it verbatim.
Compute Configuration
Whether compute (batch jobs and interactive applications) is enabled is determined by your Replicated license, not by a value you set — if your license does not include compute, these settings have no effect. If you are not sure whether your installation is entitled to compute, or you would like to add it, contact JuliaHub support to confirm your entitlement.
The compute and serviceAccount blocks in the values file above cover this. It requires an EFS access point for job userdata, an S3 bucket, and the IAM roles — all created for you by Option A, or listed in Option B: Existing EKS Cluster.
The Terraform modules attach a filesystem policy restricting mounts to the cluster node roles — restrict_efs_mounts_to_node_roles defaults to true. Every mount must then authenticate, which is why useIAM: true is set above on bothconfigDirectory.efs and compute.userdataDirectory.efs. The two must agree with each other and with the policy: without IAM the mount helper sends no credentials, EFS treats the client as anonymous, and the mount fails with access denied by server, leaving fssrvr, jobloops and gitaly unable to start.
Set both to false if your filesystems have no policy — an unpoliced EFS grants access to any client that can reach a mount target, so the mount target security group is the only control. If you used the modules, read terraform output -raw efs_mounts_require_iam and use that value for both.
IAM mounts need a platform new enough to pass the iam option on the per-job volumes it creates dynamically: 26.4.0-rc20 or later, or a 26.3 patch after 26.3.8 (JuliaHub#23608). On anything older, set restrict_efs_mounts_to_node_roles = false and useIAM: false.
Note also that EFS does not enforce IAM principal conditions for NFS mounts at all — only aws:SecureTransport, aws:SourceIp, elasticfilesystem:AccessPointArn and elasticfilesystem:AccessedViaMountTarget are honoured (AWS documentation), so the policy adds an identity check on top of the security groups rather than replacing them.
You do not need to create or host a container registry (such as Amazon ECR) for the base job images. Like the platform images, they are served through the Replicated registry, and the pull credentials are injected from your Replicated license automatically — so no registry values or image pull secrets need to be set for compute.
The role ARNs under compute.storage.aws and compute.cloudhost.aws are passed to the JuliaHub runtime, but the platform assumes AWS identity through the ServiceAccount annotation above. Configure an IRSA (or EKS Pod Identity) role whose trust policy allows the platform ServiceAccount to assume it, and which is permitted to assume the storage and cloud-host roles.
The exact permissions these roles require are install-specific (they depend on your bucket name, prefixes, account ID, and cluster OIDC provider) and they change from one JuliaHub release to the next, so this guide does not publish policy documents to copy. Contact JuliaHub support for the IAM policy JSON matching your JuliaHub version. The categories below are illustrative only — use them to plan, not as an authoritative policy:
- Storage role (
compute.storage.aws.storageRoleArn): scopeds3:*object and list access to the results bucket and its dataset/results prefixes. - Cloud-host role (
compute.cloudhost.aws.roleArn): permissions to launch and manage compute (EC2, SSM, CloudWatch Logs,iam:PassRole) and to read job secrets. - Platform role (the IRSA/Pod Identity role above):
sts:AssumeRoleon the storage and cloud-host roles, EFS mount/access-point access, ECR read, and CloudWatch Logs. If you enable the optional job-log or secrets components described under Additional Components, this role needs the additional permissions noted there — writing job logs needslogs:CreateLogStreamas well aslogs:PutLogEvents, since a log stream is created per job.
Node Provisioning for Jobs
Compute jobs and interactive applications run as pods that are scheduled onto worker nodes on demand — a single job can request a large CPU or GPU instance that only needs to exist while the job runs. Your EKS cluster therefore needs a node autoscaler to bring nodes up and down as jobs are submitted, or jobs will stay Pending with nothing to run them.
- Karpenter (recommended). JuliaHub runs Karpenter in its own managed clusters, so it is the best-tested option. Karpenter provisions right-sized nodes per pending pod and consolidates them when idle.
- Cluster Autoscaler is a lighter-weight alternative that scales pre-defined managed node groups. It works, but JuliaHub does not run it itself, so Karpenter is preferred for the elastic, heterogeneous instance sizes that jobs request.
Job pods carry no node selector for a particular pool, so they schedule onto whatever capacity the autoscaler can provide. What matters is that the autoscaler is allowed to launch an instance type large enough for the spec a job asks for — a job requesting 32 vCPU / 256 GiB stays Pending if no NodePool lists an instance type that size, whatever labels are set.
The juliarun/schedule: "yes" label on the jobs NodePool above is therefore not what routes jobs to it; it marks the pool as job capacity, matching the convention JuliaHub uses in its own managed clusters. The value of a separate pool is the separation itself: platform pods and job pods draw from different capacity, with their own limits and consolidation settings, so a burst of large jobs cannot starve the services that schedule them.
If you want to keep the platform's own pods on particular nodes (for example, a dedicated system node group separate from job capacity), the chart exposes a top-level nodeSelector and tolerations that apply to all pods, and each service accepts its own nodeSelector/tolerations that override the global ones. Set these in your values file to match the labels and taints on your chosen nodes.
Karpenter decides whether a pod fits an instance type before any node exists, so it estimates allocatable memory by subtracting vmMemoryOverheadPercent from the instance's published memory. Its 0.075 default is calibrated for Amazon Linux 2.
The EC2NodeClass in step 6 sets amiFamily: Bottlerocket, which reports roughly 3% overhead, so the install command sets 0.04 — observed overhead plus a small margin. If you use a different AMI family, adjust it to match: too high wastes capacity by provisioning larger instances than needed, and too low is worse, because the node launches and the pod then fails to fit on it.
Left at the default on Bottlerocket, Karpenter's estimate lands about 2 GiB below what the kubelet advertises on an r6i.2xlarge, and a job sized to fill an 8-vCPU / 64 GiB node never schedules — Karpenter logs no instance type has enough resources even though the instance is large enough.
JuliaHub's own Karpenter NodePool and EC2NodeClass manifests are internal to its managed infrastructure and are tuned per cluster (instance families, AMIs, capacity type, subnet/security-group discovery tags). They are not shipped as a copy-and-apply template. Work with JuliaHub support to size the CPU and GPU pools for your expected workloads.
Additional Components
Job logs and sysimage builds ship as subcharts that are off by default in the chart and should be turned on for an AWS installation. They are not optional features of the product — a job's log pane and sysimage-building jobs are both baseline functionality — but the chart cannot enable them for you, because each needs values it has no way to derive (a region, an IAM role ARN, a log group name) and each deploys cluster-scoped objects.
Treat them as part of a normal AWS install and enable both, unless one of these applies:
- A second JuliaHub installation in the same cluster. The cluster-scoped objects must only be created once.
- Logging or storage drivers managed elsewhere. If the cluster already runs its own log collector, or the CSI driver is deployed by other means, leave the corresponding component disabled.
- The compute module was applied without
create_logging. There is then no log group to write to; see the warning under Job Logs.
Nothing fails at install time when they are disabled — the feature is simply unavailable, and the symptom does not point at the cause — so decide before you install rather than after someone reports it broken.
Secrets, covered last, needs no configuration: it is enabled by default and is listed only for the IAM permission it depends on.
Job Logs
Enable this on an AWS installation. Without it a job's log pane is empty, and there is no error to explain why: nothing writes the logs, and nothing tells the platform where to read them.
Two settings are needed, and both, or job logs stay broken either way: logging-fluentbit writes the logs to CloudWatch, and compute.joblogs tells the platform which log group to read them back from.
Give both the log group the terraform module created. It is exposed as the job_log_group_name output, and the module scopes the logging IAM policy to exactly that group:
$ terraform output -raw job_log_group_name
<cluster>-<install>-job-logslogging-fluentbit:
enabled: true
# Required. Normally the same region as compute.cloudhost.aws.region; it has
# to be repeated here because Helm cannot copy one value onto another.
region: "<region>"
# The platform IRSA role, which the terraform modules grant CloudWatch Logs
# write access. Without it the collector falls back to the node role and
# silently drops every batch.
serviceAccountRoleArn: "arn:aws:iam::<account-id>:role/<platform-role>"
# The `job_log_group_name` terraform output, verbatim.
logGroupName: "<cluster>-<install>-job-logs"
compute:
joblogs:
enabled: true
# Must be the same group as logging-fluentbit.logGroupName above.
groupName: "<cluster>-<install>-job-logs"Left empty, each side composes a name from clusterName and installName, which default to the release namespace. That is a guess, and it is made twice independently. It does not have to match the group terraform created and scoped the IAM policy to — when it does not, Fluent Bit is denied on CreateLogStream for a group that does not exist, and the platform reads an empty one. Neither side reports an error. Setting logGroupName and groupName to the terraform output avoids the guess entirely.
The clusterName and installName values still exist for installations that relied on the composed name. If you use them, they must match on both sides.
The job log group and the IAM policy that permits writing to it are only created when the compute module is applied with create_logging. Without them there is nothing to write to; leave logging-fluentbit disabled on such an installation. If you are using your own IAM policies rather than the modules, the collector's role needs logs:CreateLogStream, logs:PutLogEvents and logs:DescribeLogStreams on the job log group and its :* streams — CreateLogStream in particular, because a stream is created per job.
Sysimage Build Jobs
Jobs that build a sysimage mount the resulting bundle through a JuliaHub CSI driver. Without it those pods sit in FailedMount indefinitely: the driver name is simply not registered, so nothing errors and the job never starts.
shared-bundle-csi-driver:
enabled: trueLeave it disabled if the driver is already deployed to the cluster by other means — it registers a cluster-scoped CSIDriver object, and a second copy conflicts.
Secrets
Unlike the two components above, the user and job secrets API is configured by default on an AWS installation — compute.secrets.enabled is true, and the backend is written automatically when the AWS cloud-host settings are present. Nothing needs adding to your values file.
It is listed here for the permission it depends on. The platform role needs Secrets Manager access (CreateSecret, GetSecretValue, PutSecretValue, DeleteSecret, DescribeSecret, TagResource, UpdateSecret, and ListSecrets), which the terraform modules grant. Without it the secrets API returns AccessDenied on every call. Set compute.secrets.enabled: false to leave the backend unconfigured where the role deliberately has no such access; the API then reports Secrets manager not configured with HTTP 400 instead.
It is not applied on installations using MinIO rather than AWS storage.
Exposing the Platform
Option A — ALB Ingress with ACM (recommended). Front the platform with an Application Load Balancer provisioned by the AWS Load Balancer Controller, and let the ALB terminate TLS using an ACM certificate. Set offloadTLS: true (already set in the base values file above), disable the built-in LoadBalancer Service, and enable the Ingress with your ACM certificate ARN:
websrvr:
external:
enabled: false
ingress:
enabled: true
className: "alb"
annotations:
alb.ingress.kubernetes.io/scheme: "internet-facing"
alb.ingress.kubernetes.io/target-type: "ip"
alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:<region>:<account-id>:certificate/<id>"
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'The AWS Load Balancer Controller must be installed in the cluster (see Prerequisites), and the ACM certificate must be issued for <hostname> (and *.apps.<hostname> / docs.<hostname>, or a wildcard) in the same region as the ALB.
Option B — LoadBalancer Service. If you are not using an ALB, the chart can create a LoadBalancer Service named websrvr-external, which AWS fulfils with a load balancer. With this option TLS is terminated in the cluster, so supply the certificate to the chart (tlsFullchainPem/tlsPrivkeyPem or certsSecretName) and leave offloadTLS: false. To control the AWS load balancer (for example, to request a Network Load Balancer or an internal scheme), add annotations under websrvr.annotations — these are applied to the websrvr-external Service:
websrvr:
external:
enabled: true
serviceType: "LoadBalancer"
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "external"
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "instance"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"Run the Installation
helm install juliahub-platform \
oci://registry.replicated.com/juliahub/production/juliahub-platform \
--namespace juliahub \
--create-namespace \
--timeout 30m \
--wait \
--wait-for-jobs \
--values myvalues.yamlThe initial installation may take up to 30 minutes. This includes pulling container images, starting up services, and the initial package registry sync.
DNS Configuration
Once the installation is complete, get the DNS name of the load balancer. For the recommended ALB Ingress path:
kubectl get ingress -n juliahub \
-o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}'If you used the LoadBalancer Service instead, read it from the Service:
kubectl get svc -n juliahub websrvr-external \
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}'AWS load balancers are addressed by DNS name rather than a static IP, so create the following as CNAME records (or Route 53 alias records pointing at the load balancer):
| Record | Type | Value |
|---|---|---|
<hostname> | CNAME / alias | <load-balancer-dns-name> |
*.apps.<hostname> | CNAME / alias | <load-balancer-dns-name> |
docs.<hostname> | CNAME / alias | <load-balancer-dns-name> |
In Route 53, prefer alias records targeting the load balancer over CNAMEs — aliases work at the zone apex and incur no extra lookup. A wildcard alias covers *.apps.<hostname>.
DNS propagation may take time depending on your DNS provider. JuliaHub will not be accessible until these records resolve.
Verify the Installation
1. Check that all pods are running:
kubectl get pods -n juliahubAll pods should be in Running or Completed status.
2. Access JuliaHub:
Open https://<hostname> in your browser. You should see the JuliaHub login page.
Upgrading
To upgrade to a newer version of JuliaHub:
helm upgrade juliahub-platform \
oci://registry.replicated.com/juliahub/production/juliahub-platform \
--namespace juliahub \
--timeout 30m \
--wait \
--wait-for-jobs \
--values myvalues.yaml \
--version <new-version>Next Steps
- Configure authentication for your identity provider — OIDC, SAML, LDAP, GitHub/GitLab, or an authenticating proxy
- Review the full Helm values reference for additional configuration options
- Review Admin Settings for registry, credential, and authorization configuration.
- See Common Administrative Tasks for restarts, support bundles, and volume expansion.