Skip to main content

Step 4: Deploy a Web Interface on Fargate

What This Step Does​

So far you have been querying Neptune from the command line. In this step you deploy a simple web application (built with Streamlit) so that anyone with a browser can type a query and see results — no terminal or Python knowledge required.

The app runs on AWS Fargate, a serverless container service. You provide a Docker image; Fargate runs it without you managing any servers.

Browser ──▶ Fargate (Streamlit app) ──▶ Neptune (graph database)

Shortcut: One-Command Deploy​

If you want to skip the details, run the deploy script:

export AWS_PROFILE=cloudbank-demo-admin
set -a && source .env && set +a

bash scripts/08_deploy_fargate_web_interface.sh

The script will:

  1. Build the Docker image from web_ui/.
  2. Push it to Amazon Elastic Container Registry (ECR).
  3. Create (or update) a Fargate service.
  4. Inject your NEPTUNE_ENDPOINT into the container.
  5. Wait for the service to stabilize and print a URL.

If this works, skip to Section 5 — Validate.


Prerequisites​

Before starting, make sure you have completed:

Your .env file must contain at least:

VariableExample
AWS_REGIONus-west-2
NEPTUNE_ENDPOINTcloudbank-biobricks-neptune.cluster-xxx.us-west-2.neptune.amazonaws.com

Optional overrides (defaults are fine for most users):

VariableDefault
APP_NAMEcloudbank-chemistry-ui
ECS_CLUSTERcloudbank-chemistry-ui-cluster
ECS_SERVICEcloudbank-chemistry-ui-service

Goal​

By the end of this step you have:

  • A running Fargate service
  • A URL you can open in a browser
  • A working query from the web interface to Neptune

1) Deploy with the Automation Script​

export AWS_PROFILE=cloudbank-demo-admin
set -a && source .env && set +a

bash scripts/08_deploy_fargate_web_interface.sh

Expected output (at the end):

Deploy image tag: <tag>
Deployment complete
Service URL: http://<alb-dns-or-public-ip>

Notes:

  • The first deploy can take several minutes.
  • Re-running the same script updates the running service with a new image.
  • You can pass a custom image tag: IMAGE_TAG=my-tag bash scripts/08_deploy_fargate_web_interface.sh

2) (Optional) Manual Deployment Outline​

If you prefer to deploy manually or the script does not fit your environment:

  1. Build the Docker image from web_ui/Dockerfile.
  2. Push the image to an ECR repository.
  3. Register an ECS task definition that sets NEPTUNE_ENDPOINT as an environment variable.
  4. Create a Fargate service in the same VPC as Neptune.
  5. Open port 8501 (Streamlit's default) in the service's security group.

3) Open the Web Interface​

Open the URL printed by the deploy script in your browser:

http://<public-ip>:8501

You should see:

  • A page titled Chemistry Knowledge Graph Interface
  • The AWS region and Neptune endpoint the app is connected to
  • A text box pre-filled with the sample SPARQL query
  • A Run query button and a Restore sample query button

Click Run query. If results appear in a table below, everything is working. For everything the interface can do, see Section 4.


4) What You Can Do in the Interface​

The interface is intentionally simple: it is a browser front end for the same Neptune SPARQL endpoint you used from the command line, and the whole app is just web_ui/app.py. Everything you do here is read-only: the app only sends SPARQL SELECT queries to Neptune over HTTPS and shows the results, it never modifies the graph.

Run your own queries​

  • Type or paste any SPARQL query in the text box and click Run query. The query is posted to https://<NEPTUNE_ENDPOINT>:8182/sparql, signed with the AWS credentials of the container's task role, so no keys ever reach your browser.
  • Restore sample query puts the built-in sample query back in the text box (after you edited it) and runs it right away.
  • If the query is invalid, the app shows the error returned by Neptune instead of a result table.

What the sample query does​

The default query is a real research question on OREGANO: it finds compounds that have a Wikipedia-linked name, and for each one also returns one extra predicate/value pair, with valueIsIRI / valueIsLiteral flags so you can tell whether that extra value is a link to another node or plain text.

Read the results​

  • Results appear in a table, and the ?s ?p ?o columns are renamed to Subject / Predicate / Object to match the RDF triple vocabulary from Step 3.
  • The HTTP status of the Neptune call is shown above the table, which helps tell a working query from a fancier-looking failure.
  • The Raw JSON expander shows the exact JSON Neptune returned (first 6000 characters), handy when you need the full URIs.

Common queries to try​

These queries work regardless of your domain expertise:

Get 25 random facts:

SELECT * WHERE { ?s ?p ?o } LIMIT 25

Count all facts in the database:

SELECT (COUNT(*) AS ?total) WHERE { ?s ?p ?o }

List the types of relationships available:

SELECT DISTINCT ?p WHERE { ?s ?p ?o } LIMIT 50

5) Validate the Deployment​

Check service status​

aws ecs describe-services \
--region "$AWS_REGION" \
--cluster "${ECS_CLUSTER:-cloudbank-chemistry-ui-cluster}" \
--services "${ECS_SERVICE:-cloudbank-chemistry-ui-service}" \
--query 'services[0].{Status:status,Running:runningCount,Desired:desiredCount}' \
--output table
  • Status should be ACTIVE
  • Running should equal Desired (typically 1)

Check recent logs​

aws logs tail "/ecs/${APP_NAME:-cloudbank-chemistry-ui}" \
--region "$AWS_REGION" \
--since 10m

Browser test​

  1. Open the URL in your browser.
  2. Leave the default query or type SELECT * WHERE { ?s ?p ?o } LIMIT 25.
  3. Click Run query.
  4. Confirm you see a table of results.

6) Update or Redeploy​

To deploy code changes (e.g., after editing web_ui/app.py):

bash scripts/08_deploy_fargate_web_interface.sh

The script builds a new image and triggers a rolling deployment.


7) Clean Up​

To remove the Fargate service and stop ongoing costs:

bash scripts/09_destroy_fargate_web_interface.sh

This removes the ECS service and the supporting resources the deploy script created for this demo: the Application Load Balancer and target group (if you created them), the ECS cluster, the task definitions, the ECR repository, the log group, and the security groups. Neptune is left untouched.

Safety note: the cleanup scripts only delete resources that carry the tag cloudbank-demo-created=true, which the setup and deploy scripts add to everything they create. If a security group or IAM role with one of the tutorial names already existed before you started (for example from an earlier run or another workload), the scripts print Skipping ... pre-existing and leave it untouched. The role ecsTaskExecutionRole is only deleted when this tutorial created it and no ECS cluster still uses it.

Teardown is asynchronous. Fargate stops the running tasks in the background, so the cluster can refuse deletion for a few minutes. The script now scales the service to zero, waits until the task list is empty, retries DeleteCluster (up to 10 times, 20 seconds apart), and re-checks every resource with describe before printing Fargate web interface cleanup complete (verified). Allow several minutes on the first run.

The script is safe to re-run. If it prints a WARNING and exits non-zero (teardown was still in progress, for example the cluster is still ACTIVE), wait a few minutes and run it again; the re-run completes any leftover.

Verify before declaring clean. The commands below should all return no output when cleanup has finished:

aws ecs describe-clusters --region "$AWS_REGION" --clusters "${ECS_CLUSTER:-cloudbank-chemistry-ui-cluster}"
aws ecs describe-services --region "$AWS_REGION" --cluster "${ECS_CLUSTER:-cloudbank-chemistry-ui-cluster}" --services "${ECS_SERVICE:-cloudbank-chemistry-ui-service}"
aws elbv2 describe-load-balancers --region "$AWS_REGION" --names "${ALB_NAME:-cloudbank-chemistry-ui-alb}"
aws elbv2 describe-target-groups --region "$AWS_REGION" --names "${TG_NAME:-cloudbank-chemistry-ui-tg}"
aws ecr describe-repositories --region "$AWS_REGION" --repository-names "${ECR_REPO:-cloudbank-chemistry-ui}"
aws logs describe-log-groups --region "$AWS_REGION" --log-group-name-prefix "/ecs/${APP_NAME:-cloudbank-chemistry-ui}"
aws ec2 describe-security-groups --region "$AWS_REGION" --filters "Name=group-name,Values=${UI_SG_NAME:-cloudbank-chemistry-ui-sg},${ALB_SG_NAME:-cloudbank-chemistry-ui-alb-sg}"

Notes:

  • The load balancer and target group commands apply only if you created them in Section 8.
  • An INACTIVE ECS cluster record is fine; AWS removes it by itself.
  • The security group commands show nothing for the tutorial groups. A pre-existing group with the same name is never deleted, so it still appears, and that is expected.
  • If you also ran the Step 3 cleanup (script 10), aws neptune describe-db-clusters, aws neptune describe-db-instances and aws ec2 describe-vpc-endpoints must also return nothing; the full list is in the Step 3 cleanup section.

8) (Optional) Get a Stable URL with a Load Balancer​

By default, the app is assigned a public IP that changes every time ECS replaces the container (e.g., during a new deployment). To get a stable URL without buying a domain, put the service behind an Application Load Balancer (ALB).

What you get​

  • A persistent DNS name like http://cloudbank-chemistry-ui-alb.us-west-2.elb.amazonaws.com
  • The URL stays the same across deployments
  • Easy path to add HTTPS later

Create the load balancer​

Run this block once (safe to re-run):

export AWS_PROFILE=cloudbank-demo-admin
export AWS_REGION=us-west-2

APP_NAME=cloudbank-chemistry-ui
ECS_CLUSTER=cloudbank-chemistry-ui-cluster
ECS_SERVICE=cloudbank-chemistry-ui-service
CONTAINER_NAME=cloudbank-chemistry-ui
CONTAINER_PORT=8501
ALB_NAME=cloudbank-chemistry-ui-alb
TG_NAME=cloudbank-chemistry-ui-tg
ALB_SG_NAME=cloudbank-chemistry-ui-alb-sg

# Auto-detect VPC and subnets from the existing ECS service
VPC_ID=$(aws ecs describe-services --region "$AWS_REGION" --cluster "$ECS_CLUSTER" --services "$ECS_SERVICE" \
--query 'services[0].networkConfiguration.awsvpcConfiguration.subnets[0]' --output text \
| xargs -I{} aws ec2 describe-subnets --region "$AWS_REGION" --subnet-ids {} \
--query 'Subnets[0].VpcId' --output text)
SUBNET_IDS=$(aws ecs describe-services --region "$AWS_REGION" --cluster "$ECS_CLUSTER" --services "$ECS_SERVICE" \
--query 'services[0].networkConfiguration.awsvpcConfiguration.subnets' --output text)
UI_SG_ID=$(aws ecs describe-services --region "$AWS_REGION" --cluster "$ECS_CLUSTER" --services "$ECS_SERVICE" \
--query 'services[0].networkConfiguration.awsvpcConfiguration.securityGroups[0]' --output text)

# Create ALB security group (allows inbound HTTP on port 80)
ALB_SG_ID=$(aws ec2 describe-security-groups --region "$AWS_REGION" \
--filters Name=group-name,Values="$ALB_SG_NAME" Name=vpc-id,Values="$VPC_ID" \
--query 'SecurityGroups[0].GroupId' --output text)
if [ -z "$ALB_SG_ID" ] || [ "$ALB_SG_ID" = "None" ]; then
ALB_SG_ID=$(aws ec2 create-security-group --region "$AWS_REGION" \
--group-name "$ALB_SG_NAME" --description "ALB SG for chemistry UI" \
--vpc-id "$VPC_ID" \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=cloudbank-demo-created,Value=true}]' \
--query 'GroupId' --output text)
fi
aws ec2 authorize-security-group-ingress --region "$AWS_REGION" \
--group-id "$ALB_SG_ID" --protocol tcp --port 80 --cidr 0.0.0.0/0 >/dev/null 2>&1 || true
aws ec2 authorize-security-group-ingress --region "$AWS_REGION" \
--group-id "$UI_SG_ID" --protocol tcp --port "$CONTAINER_PORT" \
--source-group "$ALB_SG_ID" >/dev/null 2>&1 || true

# Create or reuse the ALB
ALB_ARN=$(aws elbv2 describe-load-balancers --region "$AWS_REGION" --names "$ALB_NAME" \
--query 'LoadBalancers[0].LoadBalancerArn' --output text 2>/dev/null || true)
if [ -z "$ALB_ARN" ] || [ "$ALB_ARN" = "None" ]; then
ALB_ARN=$(aws elbv2 create-load-balancer --region "$AWS_REGION" --name "$ALB_NAME" \
--subnets $SUBNET_IDS --security-groups "$ALB_SG_ID" \
--scheme internet-facing --type application --ip-address-type ipv4 \
--query 'LoadBalancers[0].LoadBalancerArn' --output text)
fi

# Create or reuse the target group
TG_ARN=$(aws elbv2 describe-target-groups --region "$AWS_REGION" --names "$TG_NAME" \
--query 'TargetGroups[0].TargetGroupArn' --output text 2>/dev/null || true)
if [ -z "$TG_ARN" ] || [ "$TG_ARN" = "None" ]; then
TG_ARN=$(aws elbv2 create-target-group --region "$AWS_REGION" --name "$TG_NAME" \
--protocol HTTP --port "$CONTAINER_PORT" --target-type ip --vpc-id "$VPC_ID" \
--health-check-protocol HTTP --health-check-path / --matcher HttpCode=200-399 \
--query 'TargetGroups[0].TargetGroupArn' --output text)
fi

# Create or update the HTTP listener
LISTENER_ARN=$(aws elbv2 describe-listeners --region "$AWS_REGION" --load-balancer-arn "$ALB_ARN" \
--query 'Listeners[?Port==`80`].ListenerArn | [0]' --output text)
if [ -z "$LISTENER_ARN" ] || [ "$LISTENER_ARN" = "None" ]; then
aws elbv2 create-listener --region "$AWS_REGION" --load-balancer-arn "$ALB_ARN" \
--protocol HTTP --port 80 \
--default-actions Type=forward,TargetGroupArn="$TG_ARN" >/dev/null
else
aws elbv2 modify-listener --region "$AWS_REGION" --listener-arn "$LISTENER_ARN" \
--default-actions Type=forward,TargetGroupArn="$TG_ARN" >/dev/null
fi

# Point the ECS service at the load balancer and trigger a new deployment
aws ecs update-service \
--region "$AWS_REGION" \
--cluster "$ECS_CLUSTER" \
--service "$ECS_SERVICE" \
--load-balancers targetGroupArn="$TG_ARN",containerName="$CONTAINER_NAME",containerPort="$CONTAINER_PORT" \
--force-new-deployment >/dev/null

aws ecs wait services-stable --region "$AWS_REGION" --cluster "$ECS_CLUSTER" --services "$ECS_SERVICE"

# Print the stable URL
aws elbv2 describe-load-balancers --region "$AWS_REGION" --load-balancer-arns "$ALB_ARN" \
--query 'LoadBalancers[0].DNSName' --output text

Validate​

Replace <alb-dns> with the DNS name printed above:

curl -I "http://<alb-dns>"

Expected: HTTP 200 OK. Open the same URL in your browser to use the web interface.