Declarative Schema
Purpose
Brings custom context into SubImage's graph without writing a bespoke integration. Typical uses:
- service catalogs
- owner and team mappings
- business criticality
- application inventories
- internal CMDB or platform metadata
The module reads:
- one or more YAML schema files from S3 or GCS that describe node labels, stored properties, and relationships
- JSONL data files from S3 or GCS that contain the records for each node type in each schema
Records are loaded into Neo4j so they can join with existing cloud, identity, and security data.
How It Works
Declarative Schema is declarative, not arbitrary ETL:
- node_properties — fields persisted on the node. Must include id.
- relationships — edges matched via source fields in your JSONL records against target-node fields.
- data_path — S3 or GCS URI of the JSONL file for that node type.
- schema_id — a module-config identifier you provide so SubImage can track this schema internally.
- aws_account_id — an optional per-schema field used only when that schema is stored in S3.
Customers define only their real node types and relationships. SubImage creates the internal DeclarativeSchemaRoot and RESOURCE links automatically during sync.
Join fields such as team_id and aws_account_id do not need to appear in node_properties unless you also want them stored on the node.
| Concept | What it means |
|---|---|
node_properties |
Fields that are persisted on the node. Must include id. |
relationships |
Edge definitions that match source fields to target node fields. |
data_path |
s3://... or gs://... URI to the JSONL file for that node type. Every data_path inside one schema file must use the same provider as that schema file's schema_path. |
schema_id |
A required module-config value you choose, using lowercase letters, numbers, hyphens, and underscores. It must be unique within the Declarative Schema module config. |
aws_account_id |
A schema-entry config field used only for S3-backed schemas. |
| Schema entry field | Description |
|---|---|
schema_path |
Full schema path, such as s3://bucket/schema.yaml or gs://bucket/schema.yaml. |
schema_id |
A schema identifier you provide, such as truck_inventory or hr_assets. Each schema entry in the module config must use a unique value. |
aws_account_id |
12-digit AWS account ID that owns the S3 bucket for this schema entry. SubImage assumes SubImageDeclarativeSchemaRole in this account. Only configure this for S3-backed schemas. |
You can configure multiple schema entries in one Declarative Schema module.
- different schema entries can use different storage providers, such as one schema in S3 and another in GCS
- a single schema file cannot mix providers
- every
data_pathinside that schema file must stay on the same provider as itsschema_path
Example
- node_label: Service
node_properties:
- id
- name
- criticality
schema_version: 1
relationships:
- rel_label: RUNS_IN_ACCOUNT
target_node_label: AWSAccount
field: aws_account_id
target_field: id
direction_inward: false
data_path: s3://your-bucket/data/services.jsonlExample JSONL for Service Nodes:
{"id":"svc-001","name":"checkout-api","criticality":"high","aws_account_id":"123456789012"}Setup Steps
Option A — S3
For AWS, Declarative Schema needs to use one cross-account access path:
- role name: SubImageDeclarativeSchemaRole
- external ID: subimage-declarative-schema-
<TENANT_ID>
SubImage derives the role ARN from the AWS account ID you provide in the module config, so you do not need to paste a full IAM role ARN.
Step 1. Deploy SubImageDeclarativeSchemaRole
Deploy the role in the AWS account that owns the schema and data bucket. The trust policy should only allow sts:AssumeRole from the SubImage principal for your tenant, and should require the tenant-specific external ID subimage-declarative-schema-<TENANT_ID>.
Option A — CloudFormation
AWSTemplateFormatVersion: "2010-09-09"
Description: IAM Role and Policy for SubImage Declarative Schema bucket access
Resources:
SubImageDeclarativeSchemaRole:
Type: AWS::IAM::Role
Properties:
RoleName: SubImageDeclarativeSchemaRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS:
- arn:aws:iam::<ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly
Action: sts:AssumeRole
Condition:
StringEquals:
sts:ExternalId: subimage-declarative-schema-<TENANT_ID>
AllowSubImageDeclarativeSchemaAccess:
Type: AWS::IAM::Policy
Properties:
PolicyName: AllowSubImageDeclarativeSchemaAccess
Roles:
- !Ref SubImageDeclarativeSchemaRole
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: ReadDeclarativeSchemaObjects
Effect: Allow
Action:
- s3:GetObject
Resource:
- arn:aws:s3:::{{BUCKET_NAME}}/schema.yaml
- arn:aws:s3:::{{BUCKET_NAME}}/data/*
Outputs:
SubImageDeclarativeSchemaRoleArn:
Description: ARN of the created Declarative Schema IAM role.
Value: !GetAtt SubImageDeclarativeSchemaRole.ArnOption B — Terraform
resource "aws_iam_role" "subimage_declarative_schema" {
name = "SubImageDeclarativeSchemaRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
AWS = [
"arn:aws:iam::<ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly",
]
}
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"sts:ExternalId" = "subimage-declarative-schema-<TENANT_ID>"
}
}
}]
})
}
resource "aws_iam_role_policy" "subimage_declarative_schema" {
name = "AllowSubImageDeclarativeSchemaAccess"
role = aws_iam_role.subimage_declarative_schema.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ReadDeclarativeSchemaObjects"
Effect = "Allow"
Action = ["s3:GetObject"]
Resource = [
"arn:aws:s3:::{{BUCKET_NAME}}/schema.yaml",
"arn:aws:s3:::{{BUCKET_NAME}}/data/*",
]
}
]
})
}
output "subimage_declarative_schema_role_arn" {
value = aws_iam_role.subimage_declarative_schema.arn
}Option C — AWS CLI
aws iam create-role \
--role-name SubImageDeclarativeSchemaRole \
--assume-role-policy-document file://trust-policy.json
aws iam put-role-policy \
--role-name SubImageDeclarativeSchemaRole \
--policy-name AllowSubImageDeclarativeSchemaAccess \
--policy-document file://role-policy.jsontrust-policy.json should require sts:ExternalId = subimage-declarative-schema-<TENANT_ID>, and role-policy.json should grant only the s3:GetObject access needed for your schema object and data prefixes.
Keep the policy scoped to the exact schema object and data prefixes you plan to use. If your schema lives under a different key than schema.yaml, update the examples to match your bucket layout.
Step 2. Upload the schema and data files
Upload:
- one YAML schema file per schema entry, for example s3://your-bucket/schema.yaml
- one JSONL file per node type in that schema, for example s3://your-bucket/data/services.jsonl
Within a single schema file:
- all
data_pathentries must use the same storage provider asschema_path - for example, if
schema_pathiss3://..., then everydata_pathin that YAML file must also bes3://... - if
schema_pathisgs://..., then everydata_pathin that YAML file must also begs://...
Across multiple schema entries in the same module config, you can mix providers.
Step 3. Configure Declarative Schema in SubImage
In Modules → Declarative Schema, add one or more schema entries.
The form shows aws_account_id on every schema entry; only fill it in for S3-backed entries.
For each S3-backed schema entry, set:
- schema_path — for example
s3://your-bucket/schema.yaml - schema_id — for example
service_catalogortruck_inventory - aws_account_id — the 12-digit AWS account ID where
SubImageDeclarativeSchemaRoleis deployed for that schema's bucket
SubImage will assume:
arn:aws:iam::<aws_account_id>:role/SubImageDeclarativeSchemaRole
with tenant-specific external ID:
subimage-declarative-schema-<TENANT_ID>
For each GCS-backed schema entry, set:
- schema_path — for example
gs://your-bucket/schema.yaml - schema_id — for example
service_catalog
Leave aws_account_id blank for GCS-backed schema entries.
Option B — GCS
For GCS, Declarative Schema reuses the credentials already configured for your tenant's GCP module.
- Configure the GCP module first.
- Upload the YAML schema and JSONL data files to a GCS bucket.
- Grant the same GCP principal used by your GCP module roles/storage.objectViewer on the bucket or the relevant prefix.
- In Modules → Declarative Schema, add a schema entry with:
- schema_path (for example
gs://your-bucket/schema.yaml) - schema_id (for example
service_catalog) - no
aws_account_id
- schema_path (for example
Declarative Schema will use the existing GCP module credentials automatically.
If you configure multiple schema entries, it is valid to mix:
- an S3-backed schema entry with its own
aws_account_id - a GCS-backed schema entry that reuses your GCP module credentials
The provider only needs to stay consistent within each individual schema file.
Verification Queries
Adapt these to match the labels, relationships, and join fields in your own schema.
Confirm the internal root and RESOURCE links exist:
MATCH (root:DeclarativeSchemaRoot {id: 'service_catalog'})-[:RESOURCE]->(n)
RETURN labels(n)[0] AS label, count(n) AS count
ORDER BY labelIf you configured multiple schema entries, verify each root separately:
MATCH (root:DeclarativeSchemaRoot)
RETURN root.id AS schema_id, root.lastupdated AS lastupdated
ORDER BY schema_idVerify a custom join against an existing SubImage label:
MATCH (svc:Service)-[:RUNS_IN_ACCOUNT]->(acct:AWSAccount)
RETURN svc.name, svc.criticality, acct.id, acct.name
ORDER BY svc.nameVerify an ownership relationship:
MATCH (tm:TeamMember)
MATCH (u:AWSUser)
WHERE u.email = tm.email
RETURN tm.full_name, u.arnNotes
- Schema files are limited to 10 MB.
- Data files are limited to 30 GB.
- Use null for optional relationship fields, not empty strings.
- A single module config can manage multiple schema entries.
- Different schema entries can use different providers, but one schema file cannot mix S3 and GCS paths.
- If validation passes but a relationship is missing, check that the target label and property exist in the graph with matching values.