Skip to content

Commit 6296f8a

Browse files
authored
Merge pull request #427 from aurelianware/copilot/deploy-fhir-api-documentation
Deploy FHIR API docs (Swagger UI) and HAPI FHIR sandbox infrastructure
2 parents 18e2445 + 2e2efe8 commit 6296f8a

File tree

10 files changed

+747
-13
lines changed

10 files changed

+747
-13
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Deploy API Docs
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
paths:
7+
- 'src/api-docs/**'
8+
- 'api/openapi/**'
9+
- 'containers/api-docs/**'
10+
- '.github/workflows/deploy-api-docs.yml'
11+
workflow_dispatch:
12+
13+
permissions:
14+
id-token: write
15+
contents: read
16+
17+
env:
18+
IMAGE_NAME: api-docs
19+
ACA_NAME: api-docs
20+
ACA_ENV: cho-aca-env
21+
RESOURCE_GROUP: rg-cloudhealthoffice-prod
22+
23+
jobs:
24+
deploy:
25+
name: Build & Deploy API Docs to Azure Container Apps
26+
runs-on: ubuntu-latest
27+
28+
steps:
29+
- name: Checkout code
30+
uses: actions/checkout@v4
31+
32+
- name: Azure login via OIDC
33+
uses: azure/login@v2
34+
with:
35+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
36+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
37+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
38+
39+
- name: Log in to Azure Container Registry
40+
run: az acr login --name ${{ vars.ACR_NAME }}
41+
42+
- name: Build and push Docker image
43+
run: |
44+
IMAGE_TAG="${{ vars.ACR_LOGIN_SERVER }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
45+
IMAGE_LATEST="${{ vars.ACR_LOGIN_SERVER }}/${{ env.IMAGE_NAME }}:latest"
46+
docker build -f containers/api-docs/Dockerfile . -t "${IMAGE_TAG}" -t "${IMAGE_LATEST}"
47+
docker push "${IMAGE_TAG}"
48+
docker push "${IMAGE_LATEST}"
49+
50+
- name: Deploy to Azure Container Apps
51+
run: |
52+
IMAGE_TAG="${{ vars.ACR_LOGIN_SERVER }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
53+
az containerapp create \
54+
--name "${{ env.ACA_NAME }}" \
55+
--resource-group "${{ env.RESOURCE_GROUP }}" \
56+
--environment "${{ env.ACA_ENV }}" \
57+
--image "${IMAGE_TAG}" \
58+
--ingress external \
59+
--target-port 80 \
60+
--min-replicas 1 \
61+
--max-replicas 3 \
62+
--cpu 0.25 \
63+
--memory 0.5Gi \
64+
--registry-server "${{ vars.ACR_LOGIN_SERVER }}" 2>/dev/null || \
65+
az containerapp update \
66+
--name "${{ env.ACA_NAME }}" \
67+
--resource-group "${{ env.RESOURCE_GROUP }}" \
68+
--image "${IMAGE_TAG}"
69+
70+
- name: Verify health endpoint
71+
run: |
72+
echo "Waiting for container app to start..."
73+
sleep 30
74+
FQDN=$(az containerapp show \
75+
--name "${{ env.ACA_NAME }}" \
76+
--resource-group "${{ env.RESOURCE_GROUP }}" \
77+
--query "properties.configuration.ingress.fqdn" -o tsv)
78+
echo "Container App FQDN: ${FQDN}"
79+
curl -sf "https://${FQDN}/" -o /dev/null && \
80+
echo "✅ API Docs is healthy" || \
81+
echo "⚠️ Health check failed — container may still be warming up"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Deploy HAPI FHIR Sandbox
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
paths:
7+
- 'containers/hapi-fhir-sandbox/**'
8+
- '.github/workflows/deploy-sandbox.yml'
9+
workflow_dispatch:
10+
11+
permissions:
12+
id-token: write
13+
contents: read
14+
15+
env:
16+
ACR_REGISTRY: choacrhy6h2vdulfru6.azurecr.io
17+
IMAGE_NAME: hapi-fhir-sandbox
18+
ACA_NAME: hapi-fhir-sandbox
19+
ACA_ENV: cho-aca-env
20+
RESOURCE_GROUP: rg-cloudhealthoffice-prod
21+
22+
jobs:
23+
build-and-deploy:
24+
name: Build & Deploy HAPI FHIR Sandbox
25+
runs-on: ubuntu-latest
26+
27+
steps:
28+
- name: Checkout code
29+
uses: actions/checkout@v4
30+
31+
- name: Azure login via OIDC
32+
uses: azure/login@v2
33+
with:
34+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
35+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
36+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
37+
38+
- name: Log in to Azure Container Registry
39+
run: az acr login --name ${{ vars.ACR_NAME }}
40+
41+
- name: Build and push Docker image
42+
run: |
43+
IMAGE_TAG="${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
44+
IMAGE_LATEST="${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
45+
docker build containers/hapi-fhir-sandbox -t "${IMAGE_TAG}" -t "${IMAGE_LATEST}"
46+
docker push "${IMAGE_TAG}"
47+
docker push "${IMAGE_LATEST}"
48+
49+
- name: Deploy to Azure Container Apps
50+
run: |
51+
IMAGE_TAG="${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
52+
53+
# Scale-to-zero (min-replicas 0) for cost control.
54+
# Note: HAPI FHIR cold-start takes ~60-90s on first request after idle scale-down.
55+
az containerapp create \
56+
--name "${{ env.ACA_NAME }}" \
57+
--resource-group "${{ env.RESOURCE_GROUP }}" \
58+
--environment "${{ env.ACA_ENV }}" \
59+
--image "${IMAGE_TAG}" \
60+
--ingress external \
61+
--target-port 8080 \
62+
--min-replicas 0 \
63+
--max-replicas 2 \
64+
--cpu 0.5 \
65+
--memory 1Gi \
66+
--registry-server "${{ env.ACR_REGISTRY }}" 2>/dev/null || \
67+
az containerapp update \
68+
--name "${{ env.ACA_NAME }}" \
69+
--resource-group "${{ env.RESOURCE_GROUP }}" \
70+
--image "${IMAGE_TAG}"
71+
72+
- name: Verify health endpoint
73+
run: |
74+
echo "Waiting for container app to start..."
75+
sleep 30
76+
FQDN=$(az containerapp show \
77+
--name "${{ env.ACA_NAME }}" \
78+
--resource-group "${{ env.RESOURCE_GROUP }}" \
79+
--query "properties.configuration.ingress.fqdn" -o tsv)
80+
echo "Container App FQDN: ${FQDN}"
81+
curl -sf "https://${FQDN}/fhir/r4/metadata" -o /dev/null && \
82+
echo "✅ HAPI FHIR sandbox is healthy" || \
83+
echo "⚠️ Health check failed — container may still be warming up"

containers/api-docs/Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM nginx:1.27-alpine
2+
3+
# Add YAML MIME type (not included in nginx defaults)
4+
RUN sed -i 's|text/plain\s*txt;|text/plain txt;\n text/yaml yaml yml;|' /etc/nginx/mime.types
5+
6+
# Custom nginx configuration
7+
COPY containers/api-docs/nginx.conf /etc/nginx/conf.d/default.conf
8+
9+
# Swagger UI HTML
10+
COPY src/api-docs/index.html /usr/share/nginx/html/index.html
11+
12+
# OpenAPI YAML specs
13+
COPY api/openapi/ /usr/share/nginx/html/specs/
14+
15+
EXPOSE 80

containers/api-docs/nginx.conf

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
server {
2+
listen 80;
3+
server_name _;
4+
root /usr/share/nginx/html;
5+
index index.html;
6+
7+
# Security headers
8+
add_header X-Frame-Options "DENY" always;
9+
add_header X-Content-Type-Options "nosniff" always;
10+
add_header Access-Control-Allow-Origin "https://portal.cloudhealthoffice.com" always;
11+
12+
# Redirect /fhir/r4 paths to the Swagger UI patient-access spec
13+
location = /fhir/r4 {
14+
return 302 /?spec=patient-access;
15+
}
16+
17+
location /fhir/r4/ {
18+
return 302 /?spec=patient-access;
19+
}
20+
21+
# Serve static files; fall back to index.html for client-side routing
22+
location / {
23+
try_files $uri $uri/ /index.html;
24+
}
25+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM hapiproject/hapi:v7.4.0
2+
3+
# Copy configuration
4+
COPY application.yaml /app/config/application.yaml
5+
6+
# Copy synthetic data loader script
7+
COPY load-synthea.sh /app/load-synthea.sh
8+
RUN chmod +x /app/load-synthea.sh
9+
10+
ENV SPRING_CONFIG_LOCATION=file:///app/config/application.yaml
11+
ENV HAPI_FHIR_ALLOW_EXTERNAL_REFERENCES=true
12+
13+
EXPOSE 8080
14+
15+
ENTRYPOINT ["/app/load-synthea.sh"]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
spring:
2+
datasource:
3+
url: ${SPRING_DATASOURCE_URL:jdbc:h2:mem:hapi-fhir;DB_CLOSE_DELAY=-1;MODE=MySQL}
4+
username: sa
5+
password: ""
6+
driverClassName: org.h2.Driver
7+
jpa:
8+
properties:
9+
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
10+
hibernate:
11+
ddl-auto: update
12+
13+
hapi:
14+
fhir:
15+
fhir_version: R4
16+
server_address: https://sandbox.cloudhealthoffice.com/fhir/r4
17+
default_encoding: JSON
18+
default_pretty_print: true
19+
allow_multiple_delete: false
20+
allow_external_references: true
21+
expunge_enabled: false
22+
# Sandbox safety: limit destructive operations (no multiple delete, no expunge, no narratives)
23+
# Note: standard FHIR write operations (create/update/patch) remain enabled; full read-only must be enforced elsewhere
24+
narrative_enabled: false
25+
cors:
26+
allow_Credentials: false
27+
allowed_origin:
28+
- "*"
29+
validation:
30+
requests_enabled: false
31+
responses_enabled: false
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env bash
2+
# load-synthea.sh — Starts HAPI FHIR server and loads Synthea bundles on first boot.
3+
# The data-loaded flag file prevents re-loading on subsequent restarts.
4+
5+
set -euo pipefail
6+
7+
DATA_DIR="/data/synthea"
8+
LOADED_FLAG="/data/.synthea-loaded"
9+
HAPI_BASE_URL="http://localhost:8080/fhir"
10+
MAX_WAIT=120 # seconds to wait for HAPI to become ready
11+
12+
# ── Start HAPI in background ──────────────────────────────────────────────────
13+
echo "[load-synthea] Starting HAPI FHIR server..."
14+
/usr/local/bin/entrypoint.sh &
15+
HAPI_PID=$!
16+
17+
# ── Wait for HAPI to be ready ─────────────────────────────────────────────────
18+
echo "[load-synthea] Waiting for HAPI FHIR to be ready (up to ${MAX_WAIT}s)..."
19+
elapsed=0
20+
until curl -sf "${HAPI_BASE_URL}/metadata" -o /dev/null; do
21+
if [ $elapsed -ge $MAX_WAIT ]; then
22+
echo "[load-synthea] ERROR: HAPI FHIR did not start within ${MAX_WAIT} seconds."
23+
exit 1
24+
fi
25+
sleep 5
26+
elapsed=$((elapsed + 5))
27+
echo "[load-synthea] Still waiting... (${elapsed}s)"
28+
done
29+
echo "[load-synthea] HAPI FHIR is ready."
30+
31+
# ── Load Synthea bundles (once only) ─────────────────────────────────────────
32+
if [ -f "${LOADED_FLAG}" ]; then
33+
echo "[load-synthea] Synthetic data already loaded. Skipping."
34+
else
35+
if [ -d "${DATA_DIR}" ] && compgen -G "${DATA_DIR}/*.json" > /dev/null 2>&1; then
36+
echo "[load-synthea] Loading Synthea FHIR bundles from ${DATA_DIR}..."
37+
failed_bundles=0
38+
for bundle in "${DATA_DIR}"/*.json; do
39+
echo "[load-synthea] POST ${bundle}"
40+
if ! curl -sf \
41+
-X POST "${HAPI_BASE_URL}" \
42+
-H "Content-Type: application/fhir+json" \
43+
--data-binary "@${bundle}" \
44+
-o /dev/null; then
45+
echo "[load-synthea] ERROR: Failed to POST bundle ${bundle}"
46+
failed_bundles=$((failed_bundles + 1))
47+
fi
48+
done
49+
if [ "${failed_bundles}" -gt 0 ]; then
50+
echo "[load-synthea] Synthetic data load completed with ${failed_bundles} bundle error(s). Flag not set — will retry on next start."
51+
else
52+
touch "${LOADED_FLAG}"
53+
echo "[load-synthea] Synthetic data loaded successfully."
54+
fi
55+
else
56+
echo "[load-synthea] No Synthea bundles found in ${DATA_DIR}. Skipping data load (flag not set; will retry on next start)."
57+
fi
58+
fi
59+
60+
# ── Hand off to HAPI foreground process ───────────────────────────────────────
61+
echo "[load-synthea] Handing off to HAPI FHIR (PID ${HAPI_PID})..."
62+
wait "${HAPI_PID}"

0 commit comments

Comments
 (0)