CNP AWS Initial Setup - Small Containerized HIPAA Boundary

Purpose

This document is the setup runbook for the first CNP AWS environment.

The target system is intentionally small:

The invariant this document protects:

PHI may only exist inside the approved HIPAA perimeter: the CNP AWS account, the encrypted EC2 host, encrypted backups, and approved AWS HIPAA-eligible services covered by the executed AWS BAA.

Cloudflare Enterprise BAA coverage is assumed to be out of budget for the initial deployment. Do not route PHI-bearing traffic through Cloudflare Free, Pro, or Business plans.

Local and test environments must use generated, synthetic, deidentified, or “white” data only. No production PHI should be copied to local machines, GitHub, CI logs, test fixtures, laptops, OrbStack volumes, or developer scratch files.

Deployment Assumptions

People:

Business/operations managers should not need deep AWS knowledge. Their AWS responsibility should be limited to:

Developers own:

High-Level Architecture

                     Internet
                        |
                    Route 53
                        |
        +---------------+-------------------+
        |                                   |
Lowest cost path                 Optional AWS edge path
direct DNS to EC2                CloudFront + optional AWS WAF
        |                                   |
        +---------------+-------------------+
                        |
                AWS EC2 public IP
                        |
                Host firewall / SG
                        |
                 Reverse proxy
                        |
        +---------------+----------------+
        |               |                |
     OpenEMR         MariaDB       Python sidecar
        |               |                |
        +---------------+----------------+
                        |
              Encrypted gp3 EBS volume
                        |
              Restic encrypted backups
                        |
                  S3 backup bucket

Services To Use

Use these AWS services for the initial deployment:

Do not use Cloudflare for PHI-bearing production traffic on the initial deployment.

Cloudflare may be used only for non-PHI experiments or DNS-only experiments outside production if CNP accepts the operational complexity. Route 53 is the production default.

Do not use these by default:

Those services may be added later, but only when the operational need is explicit.

Phase 0 - Required Decisions

Before production go-live, collect:

Recommended production region:

us-east-1

Use a different region only if there is a clear data residency, latency, or operational reason.

Phase 0.5 - Developer Workstation Preflight

Do this from a developer workstation before running AWS CLI commands.

Required Tools

CLI Setup

Install or verify AWS CLI:

aws --version

Configure a named profile:

aws configure sso --profile cnp-prod

Set local shell defaults for this work session:

export AWS_PROFILE=cnp-prod
export AWS_REGION=us-east-1

Verify identity before every destructive or cost-bearing operation:

aws sts get-caller-identity

Expected result:

Local Safety Rules

Phase 1 - Create The AWS Account

Website Steps

  1. Open https://aws.amazon.com/.
  2. Create the AWS account using a CNP-owned email address, not a personal consultant address.
  3. Use a CNP-owned phone number for recovery.
  4. Add payment information.
  5. Log in as root.
  6. Enable MFA on the root user.
  7. Store root credentials and MFA recovery information in the CNP-approved password vault.
  8. Configure alternate contacts:
    • Billing: business manager
    • Operations: operations manager
    • Security: lead developer or shared security contact

CLI Verification

After a developer has CLI access:

aws sts get-caller-identity
aws account get-alternate-contact --alternate-contact-type BILLING
aws account get-alternate-contact --alternate-contact-type OPERATIONS
aws account get-alternate-contact --alternate-contact-type SECURITY

If aws account get-alternate-contact is not available for the credentials being used, verify alternate contacts in the AWS Console.

Phase 2 - Accept The AWS BAA

No PHI may enter AWS until the AWS BAA is accepted.

Website Steps

The authorized CNP representative should:

  1. Log in to the AWS Console.
  2. Open AWS Artifact.
  3. Open Agreements.
  4. Find the AWS Business Associate Addendum.
  5. Review the agreement.
  6. Accept it on behalf of CNP.
  7. Save the confirmation PDF or screenshot in CNP’s compliance records.

Console path:

AWS Console -> Artifact -> Agreements -> Business Associate Addendum

Evidence To Retain

Phase 3 - DNS And Edge Decision

Cloudflare is not the production default because its public HIPAA material says Cloudflare’s BAA is available only for Enterprise-level customers with minimum spending thresholds. That does not fit CNP’s initial cost envelope.

Use AWS-native services covered by the AWS BAA instead.

Decision

Choose one:

Mode A - lowest cost direct host
Route 53 points directly to the EC2 Elastic IP. TLS terminates on the EC2 reverse proxy.

Mode B - low-cost AWS edge
Route 53 points to CloudFront. CloudFront forwards to the EC2 origin. AWS WAF may be attached to CloudFront if the added monthly cost is approved.

Start with Mode A unless there is a concrete need for managed edge filtering.

Mode A - Lowest Cost Direct Host

Use:

Benefits:

Trade-off:

Mode B - Low-Cost AWS Edge

Use:

Benefits:

Trade-off:

Route 53 Website Steps

  1. Open Route 53.
  2. Create a public hosted zone for the CNP domain.
  3. Copy the assigned Route 53 nameservers.
  4. Log in to the domain registrar.
  5. Replace registrar nameservers with the Route 53 nameservers.
  6. Create an A record for openemr.example.org.
  7. In Mode A, point the record to the EC2 Elastic IP.
  8. In Mode B, create an alias record pointing to CloudFront.

Route 53 CLI Example

aws route53 create-hosted-zone \
  --name example.org \
  --caller-reference cnp-$(date +%Y%m%d%H%M%S)

After the hosted zone is created, update records with a change batch:

aws route53 change-resource-record-sets \
  --hosted-zone-id HOSTED_ZONE_ID \
  --change-batch file://route53-openemr-record.json

Example route53-openemr-record.json for Mode A:

{
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "openemr.example.org",
        "Type": "A",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "203.0.113.10"
          }
        ]
      }
    }
  ]
}

Optional CloudFront And AWS WAF

Add CloudFront only if CNP wants managed edge controls enough to accept the added cost and complexity.

AWS WAF cannot attach directly to a bare EC2 instance. If AWS WAF is required, place CloudFront or another supported AWS resource in front of the EC2 host.

For OpenEMR behind CloudFront:

Phase 4 - IAM Identity Center And Human Access

AWS access should be role-based and minimal.

OpenEMR user access is separate from AWS access. Operators should not receive AWS accounts.

Permission Sets

Create these AWS IAM Identity Center permission sets:

Permission Set Users Purpose
CNP-AWS-Administrator Two developers Full setup and emergency admin
CNP-AWS-DeveloperOps Two developers EC2, SSM, logs, deploy support
CNP-AWS-BillingReadOnly Business manager Cost and billing visibility
CNP-AWS-SecurityAuditReadOnly Operations manager Read-only security and audit visibility

Business and operations managers should not normally have administrator access.

Website Steps

  1. Open IAM Identity Center.
  2. Enable IAM Identity Center.
  3. Choose the identity source:
    • Prefer CNP Microsoft 365/Entra ID or Google Workspace if already managed.
    • Otherwise use the built-in Identity Center directory for the first deployment.
  4. Create users for the two developers, business manager, and operations manager.
  5. Require MFA.
  6. Create the four permission sets listed above.
  7. Assign users to permission sets.

CLI Checks

aws identitystore list-users --identity-store-id IDENTITY_STORE_ID
aws sso-admin list-permission-sets --instance-arn SSO_INSTANCE_ARN
aws sso-admin list-account-assignments \
  --instance-arn SSO_INSTANCE_ARN \
  --account-id AWS_ACCOUNT_ID \
  --permission-set-arn PERMISSION_SET_ARN

The exact IDENTITY_STORE_ID, SSO_INSTANCE_ARN, and permission set ARNs are easiest to get from the console during first setup.

Phase 5 - Business-Owned OpenEMR User Lifecycle

CNP should be able to add and revoke OpenEMR users without developer involvement.

This should happen inside OpenEMR, not AWS.

Required OpenEMR Roles

Define OpenEMR operator classes before go-live:

OpenEMR Role Intended Users AWS Access
CNP Business Manager Business manager Optional billing read-only only
CNP Operations Manager Operations manager Optional security read-only only
CNP Operator Class 1 Front-line workflow group 1 None
CNP Operator Class 2 Front-line workflow group 2 None
CNP Operator Class 3 Front-line workflow group 3 None

Website Steps In OpenEMR

After OpenEMR is installed:

  1. Log in as the initial OpenEMR administrator.
  2. Create the business manager user.
  3. Create the operations manager user.
  4. Give business/operations managers the minimum OpenEMR permissions needed to:
    • Add users.
    • Disable users.
    • Reset passwords or trigger password resets.
    • Assign users to approved operator classes.
    • Review login/audit activity.
  5. Create the three operator classes.
  6. Test that each operator class can access only the intended workflows.
  7. Document the add/revoke process for the business manager.

User Revocation Rule

Revocation must disable access in the system where the user authenticates:

Phase 6 - Cost Controls

The expected baseline cost is roughly one EC2 instance, one EBS volume, low S3 backup storage, logs, and Bedrock usage if enabled.

Budget Thresholds

Create AWS budgets at:

The $250 alert is an incident for this deployment model.

Website Steps

  1. Open AWS Billing and Cost Management.
  2. Open Budgets.
  3. Create a monthly cost budget named cnp-monthly-total.
  4. Add alert thresholds at 25, 50, 100, and 250 dollars.
  5. Notify:
    • Business manager
    • Operations manager
    • Both developers
  6. Create a second budget named cnp-bedrock-monthly if Bedrock will be used.
  7. Start the Bedrock budget low, such as $10 or $25, until usage is understood.

CLI Example

The Budgets CLI requires a JSON file. Create budget-cnp-monthly-total.json locally:

{
  "BudgetName": "cnp-monthly-total",
  "BudgetLimit": {
    "Amount": "100",
    "Unit": "USD"
  },
  "TimeUnit": "MONTHLY",
  "BudgetType": "COST"
}

Then run:

aws budgets create-budget \
  --account-id AWS_ACCOUNT_ID \
  --budget file://budget-cnp-monthly-total.json

For first setup, the website is less error-prone.

Cost Guardrails

Do not create these without explicit approval:

Tag all resources:

project=cnp
environment=prod
phi=true
owner=cnp
managed_by=opentofu

Phase 7 - Baseline Logging And Audit

The system should be self-confessing: when something fails, the logs should make the failure obvious without forensic reconstruction.

AWS Signals

Enable:

Avoid logging PHI into CloudWatch. Application logs should use metadata, not patient content.

CloudTrail Website Steps

  1. Open CloudTrail.
  2. Create a trail named cnp-account-trail.
  3. Apply to all regions.
  4. Create or select an encrypted S3 bucket for CloudTrail logs.
  5. Enable log file validation.
  6. Do not enable high-volume data events unless the cost is intentional.

CLI Example

aws s3api create-bucket \
  --bucket cnp-cloudtrail-ACCOUNT_ID-us-east-1 \
  --region us-east-1

aws cloudtrail create-trail \
  --name cnp-account-trail \
  --s3-bucket-name cnp-cloudtrail-ACCOUNT_ID-us-east-1 \
  --is-multi-region-trail \
  --enable-log-file-validation

aws cloudtrail start-logging \
  --name cnp-account-trail

Phase 8 - KMS And Encryption

Use AWS-managed encryption where that keeps the system simpler. Use customer-managed KMS keys when there is a clear operational reason, such as backup separation or explicit audit requirements.

Required Encryption

CLI Example - Create A Backup KMS Key

aws kms create-key \
  --description "CNP backup encryption key"

aws kms create-alias \
  --alias-name alias/cnp-backup-kms \
  --target-key-id KEY_ID

Keep the key policy simple. Do not lock out the administrator role.

Phase 8.5 - Secrets Management With SSM Parameter Store

Production secrets should live in AWS Systems Manager Parameter Store as SecureString values.

This keeps the production secret boundary inside the AWS account covered by the AWS BAA and avoids a separate secrets vendor.

Rules

Parameter Naming

Use one namespace per environment:

/cnp/prod/...
/cnp/stage/...
/cnp/local/...

Use production parameters like:

/cnp/prod/mariadb/root_password
/cnp/prod/mariadb/database
/cnp/prod/mariadb/user
/cnp/prod/mariadb/password
/cnp/prod/openemr/site_id
/cnp/prod/openemr/admin_user
/cnp/prod/openemr/admin_password
/cnp/prod/openemr/base_url
/cnp/prod/backup/restic_password
/cnp/prod/backup/restic_repository
/cnp/prod/sidecar/caseworthy_base_url
/cnp/prod/sidecar/caseworthy_api_key
/cnp/prod/sidecar/bedrock_region
/cnp/prod/sidecar/bedrock_model_id

Only store /cnp/local/... in AWS if there is a specific reason. Local OrbStack should normally use .env.local with synthetic-only credentials.

Generate Initial Secret Values

Generate passwords on a developer workstation. Do not paste these commands into shared notes with output visible.

openssl rand -base64 36

Generate separate values for:

Use different values in prod, stage, and local.

For values rendered into Docker env files, prefer generated secrets without whitespace or newlines. Base64 output from openssl rand -base64 36 is acceptable.

Create MariaDB Parameters

Set shell defaults:

export AWS_PROFILE=cnp-prod
export AWS_REGION=us-east-1

Create non-secret database name and username:

aws ssm put-parameter \
  --name /cnp/prod/mariadb/database \
  --type String \
  --value openemr \
  --overwrite

aws ssm put-parameter \
  --name /cnp/prod/mariadb/user \
  --type String \
  --value openemr \
  --overwrite

Create secret passwords. Replace the placeholder values before running:

aws ssm put-parameter \
  --name /cnp/prod/mariadb/root_password \
  --type SecureString \
  --value 'REPLACE_WITH_GENERATED_ROOT_PASSWORD' \
  --overwrite

aws ssm put-parameter \
  --name /cnp/prod/mariadb/password \
  --type SecureString \
  --value 'REPLACE_WITH_GENERATED_OPENEMR_DB_PASSWORD' \
  --overwrite

Create OpenEMR Parameters

aws ssm put-parameter \
  --name /cnp/prod/openemr/site_id \
  --type String \
  --value default \
  --overwrite

aws ssm put-parameter \
  --name /cnp/prod/openemr/admin_user \
  --type String \
  --value admin \
  --overwrite

aws ssm put-parameter \
  --name /cnp/prod/openemr/admin_password \
  --type SecureString \
  --value 'REPLACE_WITH_GENERATED_OPENEMR_ADMIN_PASSWORD' \
  --overwrite

aws ssm put-parameter \
  --name /cnp/prod/openemr/base_url \
  --type String \
  --value 'https://openemr.example.org' \
  --overwrite

The OpenEMR bootstrap administrator password must be changed or disabled according to the OpenEMR hardening runbook after business/operations administrator users are created.

Create Backup Parameters

aws ssm put-parameter \
  --name /cnp/prod/backup/restic_password \
  --type SecureString \
  --value 'REPLACE_WITH_GENERATED_RESTIC_PASSWORD' \
  --overwrite

aws ssm put-parameter \
  --name /cnp/prod/backup/restic_repository \
  --type String \
  --value 's3:s3.amazonaws.com/cnp-prod-backups-ACCOUNT_ID/restic' \
  --overwrite

Create Sidecar Parameters

Create these when the corresponding integrations are ready:

aws ssm put-parameter \
  --name /cnp/prod/sidecar/caseworthy_base_url \
  --type String \
  --value 'https://caseworthy.example.org' \
  --overwrite

aws ssm put-parameter \
  --name /cnp/prod/sidecar/caseworthy_api_key \
  --type SecureString \
  --value 'REPLACE_WITH_CASEWORTHY_API_KEY' \
  --overwrite

aws ssm put-parameter \
  --name /cnp/prod/sidecar/bedrock_region \
  --type String \
  --value us-east-1 \
  --overwrite

aws ssm put-parameter \
  --name /cnp/prod/sidecar/bedrock_model_id \
  --type String \
  --value 'REPLACE_WITH_APPROVED_MODEL_ID' \
  --overwrite

Do not create placeholder production API keys. If an integration is not ready, leave its secret absent and make the sidecar fail clearly with a missing-configuration error.

If an optional integration is not ready, do not include it in the production render script yet. Required OpenEMR, MariaDB, and backup parameters should be present before go-live.

Verify Parameter Names Without Printing Secrets

List names:

aws ssm get-parameters-by-path \
  --path /cnp/prod \
  --recursive \
  --query 'Parameters[].Name' \
  --output text

Verify one secret exists without returning decrypted value:

aws ssm get-parameter \
  --name /cnp/prod/mariadb/password \
  --no-with-decryption \
  --query 'Parameter.{Name:Name,Type:Type,Version:Version,LastModifiedDate:LastModifiedDate}'

Only retrieve decrypted values on the EC2 host or during controlled break-glass operations:

aws ssm get-parameter \
  --name /cnp/prod/mariadb/password \
  --with-decryption \
  --query 'Parameter.Value' \
  --output text

EC2 Instance Role Policy

Attach a policy like this to the EC2 instance role. Scope ACCOUNT_ID, region, and paths exactly.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters",
        "ssm:GetParametersByPath"
      ],
      "Resource": "arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/cnp/prod/*"
    }
  ]
}

If using a customer-managed KMS key for SecureString values, also grant the instance role kms:Decrypt for that key.

Developer Write Policy

Developers need controlled write access to /cnp/prod/* during setup and rotation.

Prefer an IAM Identity Center permission set that allows:

Restrict the resource path to:

arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/cnp/*

Render Runtime Env File On The Host

The EC2 host should pull SSM values into a root-owned env file before starting Docker Compose.

Create:

/srv/cnp/compose/.env.prod

Permissions:

sudo touch /srv/cnp/compose/.env.prod
sudo chown root:root /srv/cnp/compose/.env.prod
sudo chmod 600 /srv/cnp/compose/.env.prod

Example render script:

#!/usr/bin/env bash
set -euo pipefail

out="/srv/cnp/compose/.env.prod"
tmp="$(mktemp)"

get_plain() {
  aws ssm get-parameter \
    --name "$1" \
    --query 'Parameter.Value' \
    --output text
}

get_secret() {
  aws ssm get-parameter \
    --name "$1" \
    --with-decryption \
    --query 'Parameter.Value' \
    --output text
}

{
  printf 'MARIADB_DATABASE=%s\n' "$(get_plain /cnp/prod/mariadb/database)"
  printf 'MARIADB_USER=%s\n' "$(get_plain /cnp/prod/mariadb/user)"
  printf 'MARIADB_PASSWORD=%s\n' "$(get_secret /cnp/prod/mariadb/password)"
  printf 'MARIADB_ROOT_PASSWORD=%s\n' "$(get_secret /cnp/prod/mariadb/root_password)"
  printf 'OPENEMR_SITE_ID=%s\n' "$(get_plain /cnp/prod/openemr/site_id)"
  printf 'OPENEMR_ADMIN_USER=%s\n' "$(get_plain /cnp/prod/openemr/admin_user)"
  printf 'OPENEMR_ADMIN_PASSWORD=%s\n' "$(get_secret /cnp/prod/openemr/admin_password)"
  printf 'OPENEMR_BASE_URL=%s\n' "$(get_plain /cnp/prod/openemr/base_url)"
  printf 'RESTIC_PASSWORD=%s\n' "$(get_secret /cnp/prod/backup/restic_password)"
  printf 'RESTIC_REPOSITORY=%s\n' "$(get_plain /cnp/prod/backup/restic_repository)"
} > "$tmp"

sudo install -o root -g root -m 600 "$tmp" "$out"
rm -f "$tmp"

Store the script itself in Git, but never store the rendered .env.prod.

Add sidecar integration values to this script only when those integrations are ready for production.

Compose Usage

Docker Compose should reference the rendered env file:

docker compose --env-file /srv/cnp/compose/.env.prod up -d

The compose file may reference variable names, but must not contain secret values.

Rotation

When rotating a secret:

  1. Put the new value into SSM Parameter Store.
  2. Render /srv/cnp/compose/.env.prod again.
  3. Restart only the affected containers.
  4. Verify health checks.
  5. Record the rotation event.

For MariaDB passwords, coordinate rotation carefully:

  1. Create or update the database user password inside MariaDB.
  2. Update the SSM parameter.
  3. Re-render env file.
  4. Restart OpenEMR.
  5. Confirm OpenEMR can connect.
  6. Remove old credentials if a temporary user was used.

Phase 9 - Network Model

Default model:

Security Group

Inbound:

Port Source Purpose
443 0.0.0.0/0 in Mode A, or CloudFront origin-facing controls in Mode B OpenEMR HTTPS
80 Temporary only for certificate bootstrap, if needed ACME challenge
22 None Do not use SSH

Outbound:

Allow HTTPS outbound for:

CLI Example

aws ec2 create-security-group \
  --group-name cnp-prod-openemr-sg \
  --description "CNP production OpenEMR host" \
  --vpc-id VPC_ID

aws ec2 authorize-security-group-ingress \
  --group-id SECURITY_GROUP_ID \
  --protocol tcp \
  --port 443 \
  --cidr 0.0.0.0/0

If CloudFront is used, configure the origin so direct-to-origin access is intentionally controlled. The simplest starting control is a strong origin secret header validated by the reverse proxy. More advanced controls can use AWS-managed prefix lists for CloudFront origin-facing traffic where appropriate.

Phase 10 - S3 Backup Bucket

Backups should be portable and restorable without AWS-specific application services.

Primary backup mechanism:

  1. Dump MariaDB.
  2. Capture OpenEMR files/configuration.
  3. Encrypt with Restic.
  4. Upload to S3.
  5. Record success/failure.
  6. Regularly test restore.

Website Steps

  1. Open S3.
  2. Create bucket named like cnp-prod-backups-ACCOUNT_ID.
  3. Block all public access.
  4. Enable bucket versioning.
  5. Enable default encryption.
  6. Add lifecycle policy:
    • Keep daily backups for 30 days.
    • Keep weekly backups for 12 weeks.
    • Keep monthly backups for 12 months.
  7. Consider Object Lock before production if the retention policy is stable.

Object Lock must be enabled at bucket creation time. If ransomware resistance is a priority, decide this before creating the final production backup bucket.

CLI Example

aws s3api create-bucket \
  --bucket cnp-prod-backups-ACCOUNT_ID \
  --region us-east-1

aws s3api put-public-access-block \
  --bucket cnp-prod-backups-ACCOUNT_ID \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

aws s3api put-bucket-versioning \
  --bucket cnp-prod-backups-ACCOUNT_ID \
  --versioning-configuration Status=Enabled

aws s3api put-bucket-encryption \
  --bucket cnp-prod-backups-ACCOUNT_ID \
  --server-side-encryption-configuration '{
    "Rules": [
      {
        "ApplyServerSideEncryptionByDefault": {
          "SSEAlgorithm": "AES256"
        }
      }
    ]
  }'

Phase 11 - EC2 Host

Instance Recommendation

Start with:

t4g.large

Rationale:

Fallback:

t3.large or t3a.large

Use x86_64 only if a required image or dependency does not support ARM64.

Storage

Recommended:

All persistent Docker volumes should live under /srv/cnp.

IAM Role For EC2

Create an instance role with only:

Do not put long-lived AWS keys on the server.

Website Steps

  1. Open EC2.
  2. Launch instance.
  3. Select Ubuntu LTS or Amazon Linux 2023 ARM64.
  4. Select t4g.large.
  5. Attach IAM instance profile.
  6. Disable public SSH access.
  7. Attach the security group from Phase 9.
  8. Create encrypted root volume.
  9. Add encrypted 100 GB gp3 data volume.
  10. Enable detailed monitoring only if the cost is acceptable.

CLI Sketch

The exact AMI ID changes by region. Resolve it first, then launch:

aws ec2 run-instances \
  --image-id AMI_ID \
  --instance-type t4g.large \
  --iam-instance-profile Name=cnp-prod-ec2-profile \
  --security-group-ids SECURITY_GROUP_ID \
  --subnet-id SUBNET_ID \
  --block-device-mappings file://block-devices.json \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=project,Value=cnp},{Key=environment,Value=prod},{Key=phi,Value=true},{Key=Name,Value=cnp-prod-openemr}]'

Prefer OpenTofu for the final version of this step once the manual shape is confirmed.

Phase 12 - Host Bootstrap

The host should be reproducible. Manual shell work is acceptable during initial discovery but must become a script before production.

Bootstrap Tasks

CLI Steps On Host

Use SSM Session Manager:

aws ssm start-session --target INSTANCE_ID

Then bootstrap:

sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install -y ca-certificates curl gnupg ufw

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo mkdir -p /srv/cnp/{compose,openemr,mariadb,sidecar,backups,logs,bin}
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 443/tcp
sudo ufw --force enable

Adapt package commands if using Amazon Linux 2023.

Phase 13 - Docker Compose Runtime

Production should use the same Compose shape as local test, with different secrets and volumes.

Production secrets come from SSM Parameter Store and are rendered into /srv/cnp/compose/.env.prod before Compose starts. The rendered env file is root-owned, mode 600, and never committed.

Required services:

Expected Compose Responsibilities

proxy:

openemr:

mariadb:

sidecar:

backup:

Minimum Health Checks

The deployment is not healthy unless these pass:

Phase 14 - Bedrock Controls

Bedrock may be used only after:

Design Rule

All Bedrock calls should go through the Python sidecar. OpenEMR should not call Bedrock directly.

This gives one place to enforce:

Do Not Log

Log Instead

CLI Example - Minimal IAM Policy Shape

Scope this to the exact foundation models and region once chosen:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "bedrock:InvokeModel",
        "bedrock:InvokeModelWithResponseStream"
      ],
      "Resource": "*"
    }
  ]
}

Start broad only during discovery. Narrow before production.

Phase 15 - Local Simulacrum On Mac Studio With OrbStack

Local test should prove the deployment process and runtime behavior without using PHI.

Local Invariant

Local data must be:

Local data must not be:

Local Runtime

Use OrbStack with the same Compose services:

Differences from production:

Local Setup Sketch

docker compose -f compose.yaml -f compose.local.yaml up -d
docker compose ps
docker compose logs --tail=100 sidecar

Required Local Confidence Checks

Before production deployment, local must prove:

Phase 16 - Code Deployment Process

The deployment process must be boring, repeatable, and observable.

Developer change
      |
      v
Pull request
      |
      v
Automated checks
      |
      v
Build container images
      |
      v
Run local OrbStack simulacrum
      |
      v
Tag release
      |
      v
Deploy to EC2 through SSM or a controlled deploy script
      |
      v
Run post-deploy health checks
      |
      v
Record deployment event

Required Checks Before Deploy

CLI Examples

Local:

docker compose config
docker compose build
docker compose up -d
docker compose ps

Production over SSM:

aws ssm start-session --target INSTANCE_ID

On host:

cd /srv/cnp/compose
docker compose pull
docker compose up -d
docker compose ps

Post-deploy:

curl -fsS https://openemr.example.org/ || exit 1
docker compose ps
docker compose logs --tail=100 sidecar

Deployment Record

Each production deployment should record:

Phase 17 - Backup And Restore

Backups are not complete until restore has been tested.

Backup Schedule

Required Backup Signals

Restore Test Cadence

Minimum:

Restore tests should use a clean environment and must not export PHI to local machines.

Phase 18 - Go-Live Checklist

Do not put PHI into the system until all required items are complete.

Required:

Phase 19 - Operator Runbooks To Deliver

Deliver these short runbooks to CNP:

Open Questions To Resolve Before Production

These do not block initial AWS setup, but they should be resolved before go-live:

References

← Back to portal