> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.astropods.com/postgres-on-eks/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.astropods.com/_mcp/server. # Host Postgres on EKS for PrivateLink This page is a working sample for [connecting a private database](/knowledge-stores#connect-a-private-database). It runs Postgres with pgvector on Amazon EKS, puts an internal NLB in front of it, and creates the VPC Endpoint Service that Astropods connects to. Adapt the names, sizes, and CIDRs to your environment. ## Prerequisites * An EKS cluster with the [AWS Load Balancer Controller](https://kubernetes-sigs.github.io/aws-load-balancer-controller/) installed. * The Amazon EBS CSI driver add-on, with an IAM role for its service account. A cluster with no stateful workloads yet often lacks it. * Private subnets in at least two Availability Zones. * `kubectl` access to the cluster and the AWS CLI for the cluster's account. The sample uses `10.0.0.0/16` as the VPC CIDR and `playbook` as the name. Replace both. #### Create the namespace, storage, and password **`postgres-storage.yaml`** ```yaml title="postgres-storage.yaml" apiVersion: v1 kind: Namespace metadata: name: playbook --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: gp3 provisioner: ebs.csi.aws.com parameters: type: gp3 volumeBindingMode: WaitForFirstConsumer allowVolumeExpansion: true --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-data namespace: playbook spec: accessModes: [ReadWriteOnce] storageClassName: gp3 resources: requests: storage: 50Gi ``` ```bash kubectl apply -f postgres-storage.yaml kubectl -n playbook create secret generic postgres-auth \ --from-literal=POSTGRES_USER=agent \ --from-literal=POSTGRES_PASSWORD="$(openssl rand -base64 24)" ``` Skip the `StorageClass` if the cluster already has a gp3 class. #### Deploy Postgres **`postgres-deployment.yaml`** ```yaml title="postgres-deployment.yaml" apiVersion: apps/v1 kind: Deployment metadata: name: postgres namespace: playbook spec: replicas: 1 strategy: type: Recreate selector: matchLabels: {app: postgres} template: metadata: labels: {app: postgres} spec: containers: - name: postgres image: pgvector/pgvector:pg17 ports: - containerPort: 5432 envFrom: - secretRef: {name: postgres-auth} env: - {name: POSTGRES_DB, value: playbook} - {name: PGDATA, value: /var/lib/postgresql/data/pgdata} securityContext: capabilities: drop: [ALL] add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID] readinessProbe: exec: command: [pg_isready, -U, agent, -d, playbook] periodSeconds: 10 resources: requests: {cpu: 500m, memory: 1Gi} volumeMounts: - {name: data, mountPath: /var/lib/postgresql/data} volumes: - name: data persistentVolumeClaim: claimName: postgres-data ``` ```bash kubectl apply -f postgres-deployment.yaml kubectl -n playbook rollout status deploy/postgres ``` Three settings in this manifest matter: * `strategy: Recreate`. An EBS volume attaches to one node at a time. A rolling update starts the new pod before the old one releases the volume, so the new pod stays `Pending`. * The capabilities. The image's entrypoint starts as root, changes the volume's owner to the `postgres` user, then switches to that user. Dropping all capabilities breaks that step and the pod crash-loops. Don't set `runAsNonRoot` or `runAsUser` for the same reason. * `PGDATA` points at a subdirectory. A new EBS volume contains `lost+found`, and Postgres refuses to initialize a non-empty directory. #### Expose Postgres on an internal NLB **`postgres-service.yaml`** ```yaml title="postgres-service.yaml" apiVersion: v1 kind: Service metadata: name: postgres namespace: playbook annotations: service.beta.kubernetes.io/aws-load-balancer-type: external service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip service.beta.kubernetes.io/aws-load-balancer-scheme: internal service.beta.kubernetes.io/load-balancer-source-ranges: 10.0.0.0/16,100.64.0.0/10 spec: type: LoadBalancer selector: {app: postgres} ports: - {name: postgres, port: 5432, targetPort: 5432} ``` ```bash kubectl apply -f postgres-service.yaml kubectl -n playbook get svc postgres \ -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' ``` The source ranges must include `100.64.0.0/10`, because PrivateLink traffic reaches the NLB from that range. Keep the VPC CIDR for callers inside the VPC. The annotation key has no `aws-` prefix: the controller ignores `aws-load-balancer-source-ranges` and allows `0.0.0.0/0`. #### Create the VPC Endpoint Service Look up the NLB's ARN from the hostname the previous step printed, then create the endpoint service: ```bash NLB_ARN=$(aws elbv2 describe-load-balancers \ --query "LoadBalancers[?DNSName==''].LoadBalancerArn" \ --output text) aws ec2 create-vpc-endpoint-service-configuration \ --network-load-balancer-arns "$NLB_ARN" \ --acceptance-required ``` Note the `ServiceId` (`vpce-svc-...`) and the `ServiceName` (`com.amazonaws.vpce..vpce-svc-...`) in the output. With `--acceptance-required`, every connection request waits for you to approve it. #### Allow Astropods to connect In the dashboard, go to **Knowledge** > **Add store** and turn **PrivateLink** on. The form shows the principal to allow and the supported region to add. Set both on the endpoint service: ```bash aws ec2 modify-vpc-endpoint-service-permissions \ --service-id \ --add-allowed-principals aws ec2 modify-vpc-endpoint-service-configuration \ --service-id \ --add-supported-regions ``` #### Connect the store and approve the request Finish the **Add store** form with the `ServiceName`, port `5432`, database `playbook`, and the credentials from step 1. You can use [`ast knowledge connect`](/cli/knowledge#knowledge-connect) instead. Astropods then requests a connection. Approve it: ```bash aws ec2 describe-vpc-endpoint-connections \ --filters Name=service-id,Values= aws ec2 accept-vpc-endpoint-connections \ --service-id \ --vpc-endpoint-ids ``` The store turns **Ready** once the connection's `VpcEndpointState` is `available`. #### Confirm the agent can query it Deploy an agent that declares a `postgres` store and choose **Shared** for it on the deploy form. Ask the agent a question that only your data can answer. **Ready** confirms the endpoint exists, not that queries reach Postgres. If queries hang and time out, the NLB is dropping PrivateLink traffic. Check the source ranges from step 3, then see [Troubleshooting](/knowledge-stores#troubleshooting). ## Next steps * [Knowledge stores](/knowledge-stores): how stores work and how to troubleshoot a connection * [Using knowledge stores](/knowledge-store-agent): declare a store in an agent and bind it at deploy time > Run Postgres with pgvector on EKS and expose it to Astropods over PrivateLink