This repository contains the AWS CDK code (written in Python) used to deploy the MAAP project eoapi infrastructure. It is based on the eoapi-template example. For the MAAP use case, we use a subset of the eoapi CDK constructs to define a database, an ingestion API, a STAC API, a raster API (i.e a tiling API) and a pgbouncer instance to manage connections to the database. Here, we deploy all these components into a custom VPC.
Deployment happens through a github workflow manually triggered and defined in .github/workflows/deploy.yaml.
The MAAP-owned STAC runtime uses stac-fastapi-pgstac[catalogs] 6.3.0. Read-only multi-tenant catalog routes are enabled by default for deployed STAC APIs. Catalog write routes and collection write routes remain explicit opt-ins.
User STAC catalog configuration:
USER_STAC_CATALOGS_ENABLED=falsedisables read-only/catalogsroutes.USER_STAC_CATALOGS_HIDE_ALTERNATE_PARENTS=truehides alternate parent links in catalog responses.USER_STAC_CATALOG_TRANSACTIONS_AUTH_MODE=basicenables catalog write routes and selects the supported auth mode. Catalog write routes require catalogs to stay enabled.USER_STAC_CATALOG_TRANSACTIONS_AUTH_SECRET_ARNcan point at an existing auth secret.
The DPS item generator assigns unregistered items to collections named
{username}__{algorithm_name}__{algorithm_version}. Authorized user-supplied
collection IDs are preserved. Generated items include the filterable
maap-dps:algorithm_name, processing:version, maap-dps:username,
and maap-dps:tag properties, the MAAP DPS STAC extension, and a dps-metadata
asset containing the source .met.json file. The generator also overwrites the
STAC Common Metadata created property with the UTC publication time shared by
all Items generated from that catalog.
For each job with generated items, the same SNS stream also receives a plain
STAC 1.1.0 user Catalog and one Collection. If the input catalog contains one
source Collection for those generated items, its useful metadata and resolved
asset links are reused with the deterministic collection ID and the URL-safe
user catalog ID as its only parent_ids value. With no source Collection, the
Collection uses whole-world, open-ended extents. Multiple source Collections
for one job fail rather than being merged. Authorized named collections remain
Item-only.
Repeated upserts overwrite manual curation on generated Catalog and Collection
records. The deployed loader still has CREATE_COLLECTIONS_IF_MISSING=TRUE,
and pgSTAC is configured to maintain collection extents from ingested Items, so
a source extent is initial metadata and may be updated asynchronously from
Items. Named collections are untouched.
To add hierarchy records for historical generated collections, preview this conservative, restartable backfill before applying it:
uv run --script scripts/backfill_dps_user_catalogs.py --dry-run
uv run --script scripts/backfill_dps_user_catalogs.py --applyThe backfill uses hydrated item metadata and actual collection IDs. It recognizes both current three-part and legacy tag-specific four-part generated IDs, and skips named, authorized, mixed, incomplete, and ambiguous collections. Historical authorization cannot always be proven when its registry is incomplete, so review the dry-run report. Existing Collection metadata is preserved; apply only adds the parent relationship and creates a missing user Catalog. It does not rewrite or rename Items.
To merge legacy tag-specific DPS collections into these tag-free IDs, preview then apply the database migration:
./scripts/migrate_dps_collection_ids.py --dry-run
./scripts/migrate_dps_collection_ids.py --applyIt recognizes four-part IDs (username__algorithm__version__tag), merges their
items into the corresponding three-part ID, and adds the DPS metadata fields
from the legacy ID. Collections containing an item-ID collision retain their
legacy ID, but their Items still receive those metadata fields. For a deployed
database, follow the RDS connection guide
below and the RDS usage instructions in the migration script's docstring.
Collection-only STAC transactions can still be enabled with:
USER_STAC_COLLECTION_TRANSACTIONS_AUTH_MODE=basic
When either collection or catalog transactions are enabled, this CDK stack creates and manages the Secrets Manager secret used for STAC basic auth by default, grants the STAC Lambda read access to it, and publishes the secret ARN to SSM at:
/maap-eoapi/<stage>/internal/stac-collection-transaction-auth-secret-arn
You can still override the secret with USER_STAC_COLLECTION_TRANSACTIONS_AUTH_SECRET_ARN or USER_STAC_CATALOG_TRANSACTIONS_AUTH_SECRET_ARN if you need to point at an existing secret instead. If both write surfaces are enabled, they must use the same secret in this iteration.
The transaction auth secret must be a JSON object with string username and password fields.
After starting the local pgSTAC database, you can load a small demo catalog hierarchy for sample users:
docker compose up -d database
./scripts/load_demo_stac_catalogs.pyThe script is standalone and uses an inline uv execution header, so it installs pypgstac[psycopg] on demand. By default it connects to the local compose database on 127.0.0.1:5439 and creates:
DPS User Catalogsas a root catalog, containing per-user catalogs forhrodmnandjjfrenchDPS Team Catalogsas a root catalog, containing the sharedmaap-demo-teamcatalog- two synthetic DPS-output collections per user
Open http://127.0.0.1:8080 to test the user STAC Browser configuration used by the deployment. Its landing page shows only root catalogs; opening a catalog shows its scoped collections.
Useful options:
./scripts/load_demo_stac_catalogs.py --dry-run
./scripts/load_demo_stac_catalogs.py --reset # deletes all existing catalog and collection records first
./scripts/load_demo_stac_catalogs.py --user hrodmn --user jjfrench
./scripts/load_demo_stac_catalogs.py --database-url postgresql://username:password@database:5432/postgisThe database hostname form is useful when running the script from a container attached to the maap-eoapi Docker network.
For a catalogs-enabled deployment, verify:
- OpenAPI includes read-only catalog routes such as
GET /catalogs,GET /catalogs/{catalog_id}, and catalog-scoped collection/item reads. GET /includesrel="child"links for listed catalogs so STAC Browser can discover catalog roots.- catalog write routes are absent unless
USER_STAC_CATALOG_TRANSACTIONS_AUTH_MODE=basicis configured.
The deployment includes a public STAC Browser and, when
USER_STAC_BROWSER_CUSTOM_DOMAIN_NAME and USER_STAC_BROWSER_CERTIFICATE_ARN are
set, a separate user-STAC Browser. Set STAC_BROWSER_REPO_TAG to v5.1.0 (or a
compatible STAC Browser v5 release) for the user browser configuration. On the
user-STAC Browser landing page, verify that catalog links are shown and the
landing page's broad rel="data" link is not. Open a child catalog and verify
that its scoped rel="data" link still lists collections, then open a
collection and verify that rel="items" lists its items. The customization
uses the browser path for root detection, not an API landing-page ID.
For a transaction-enabled internal deployment, verify:
GET /conformanceincludeshttps://api.stacspec.org/v1.0.0/collections/extensions/transactionwhen collection transactions are enabled.- OpenAPI advertises collection write routes only for collection transactions:
POST /collectionsPUT /collections/{collection_id}PATCH /collections/{collection_id}DELETE /collections/{collection_id}
- OpenAPI advertises catalog write routes only for catalog transactions, including
POST /catalogsandPUT/DELETE /catalogs/{catalog_id}. - unauthenticated writes return
401 - authenticated writes succeed
- item write routes are absent from the contract and return
404or405rather than exposing item transaction behavior
Because of security requirements, the networking set up imposes the following constraints :
- For security reasons, the database is in a private subnet of the VPC. As such, only instances running inside of the same VPC can access the database. This means that, for example, even if a user has the password and her IP is allowed inbound connections to the database, access will not be allowed.
This has three consequences :
- The APIs that need access to the database (the STAC API, the tiling API, the ingestion API) need to be deployed in that same VPC.
- In addition, because these APIs also sometimes need access to the internet, a NAT gateway must in addition be deployed in that VPC.
- For direct, administrative connections to the database, one must go through an instance placed in the same VPC as the database.
For administrative database access, use the existing PgBouncer EC2 instance as an SSM network relay and run your database client locally. Forward to the RDS endpoint, not the PgBouncer service, to bypass connection pooling. RDS stays private, and you do not need to install dependencies on the EC2 instance or open inbound ports.
You need the AWS CLI, jq, curl, and a PostgreSQL client such as psql
on your workstation. Also
install the Session Manager plugin;
it is a separate installation from the AWS CLI.
Configure your AWS profile and region first. Your AWS
identity needs permission to read the stack resources, SSM parameter, and
database secret (including KMS decryption if applicable), and start sessions
using AWS-StartPortForwardingSessionToRemoteHost. The EC2 instance must be
SSM-managed with SSM Agent 3.1.1374.0 or later.
Choose the deployment you want to connect to:
| Database | Stack name | SSM parameter type |
|---|---|---|
| User STAC (including DPS outputs) | MAAP-STAC-<stage>-userSTAC |
internal |
| Public STAC | MAAP-STAC-<stage>-pgSTAC |
public |
The examples use userSTAC. Confirm your account and stage, then list the secrets belonging to that CDK deployment:
aws sts get-caller-identity
STAGE=test # change as appropriate
STACK="MAAP-STAC-${STAGE}-userSTAC" # userSTAC or pgSTAC
aws cloudformation list-stack-resources \
--stack-name "$STACK" \
--query 'StackResourceSummaries[?ResourceType==`AWS::SecretsManager::Secret`].[LogicalResourceId,PhysicalResourceId]' \
--output tableYou can also find these under CloudFormation → stack → Resources.
Select the database secret whose ID contains pgstacdbbootstrappersecret, not the
STAC HTTP basic-auth secret. CloudFormation gives you the secret's identifier; retrieve its value
from Secrets Manager. In the same terminal:
SECRET_ID='<database secret arn from the table>'
DB_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "$SECRET_ID" --query SecretString --output text)
export PGHOST=$(jq -er '.host' <<< "$DB_SECRET")
export PGDATABASE=$(jq -er '.dbname' <<< "$DB_SECRET")
export PGUSER=$(jq -er '.username' <<< "$DB_SECRET")
export PGPASSWORD=$(jq -er '.password' <<< "$DB_SECRET")
unset DB_SECRETCheck that these commands succeed and that PGHOST matches the selected RDS
endpoint. Do not print the secret or run these commands with shell tracing
(set -x) enabled.
In a second terminal with the same AWS profile and region, retrieve the RDS endpoint from the same secret and start the session. Variables set in the first terminal are not available in this terminal:
STAGE=test # use the same stage as above
TYPE=internal # use public for the pgSTAC stack
SECRET_ID='<same database secret physical ID from the table>'
RDS_HOST=$(aws secretsmanager get-secret-value \
--secret-id "$SECRET_ID" --query SecretString --output text | jq -er '.host')
INSTANCE_ID=$(aws ssm get-parameter \
--name "/maap-eoapi/$STAGE/$TYPE/pgbouncer-instance-id" \
--query Parameter.Value --output text)
aws ssm start-session \
--target "$INSTANCE_ID" \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "{\"host\":[\"$RDS_HOST\"],\"portNumber\":[\"5432\"],\"localPortNumber\":[\"15432\"]}"Leave this terminal open while you use the database. The EC2 host needs network access to RDS on port 5432, as it does for normal PgBouncer traffic.
Back in the first terminal, download the
AWS RDS CA bundle
and configure TLS. PGHOSTADDR sends the connection through localhost while
PGHOST retains the RDS hostname for certificate verification:
curl --fail --show-error --silent \
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \
--output /tmp/maap-rds-global-bundle.pem
export PGHOSTADDR=127.0.0.1
export PGPORT=15432
export PGSSLMODE=verify-full
export PGSSLROOTCERT=/tmp/maap-rds-global-bundle.pem
psql -c 'SELECT current_database(), current_user;'
psqlpsql and other libpq-based clients, including psycopg, can use these PG*
environment variables. A client's explicit connection string can override
them; check the tool's connection options before running commands.
Use \q to leave psql. When finished, clear the connection variables and
close the SSM session in the second terminal:
unset PGPASSWORD PGHOST PGHOSTADDR PGPORT PGDATABASE PGUSER PGSSLMODE PGSSLROOTCERTBefore destructive operations, confirm the target database and ensure you have a recoverable backup. For work expected to run for hours, prefer a durable in-VPC execution environment over a workstation tunnel.
The term "ingestion" refers to the process of cataloging data in the STAC catalog associated with this deployment.
For a small record ingestion (for example a collection record or just one item), one can directly connect to the database and perform loading. This can be done using the pypgstac library. For example, to load an item stored locally in test_item.json, with pypgstac installed, you can run the following command :
pypgstac load --table items test_item.jsonor for a collection
pypgstac load --table collections test_collection.jsonFor larger scale ingestions, in MAAP we rely on a fork of the stactools-pipelines repository. If you want to ingest a collection in MAAP using this tool, you should develop a 'pipeline'. Details of this procedure can be found in the linked repository. You can follow an example that was developed for maap here.