AWS Builder Center
Data lineage you can defend, part 2: AI-assisted lineage on AWS

Data lineage you can defend, part 2: AI-assisted lineage on AWS

*How to build AI-assisted, human-validated data lineage on AWS. A hands-on guide covering native lineage capture with Amazon SageMaker Catalog, mainframe analysis with AWS Transform, a code-parsing agent on AWS Step Functions and Amazon Bedrock, a human review workflow, and validated lineage exposed to AI agents through Amazon Bedrock AgentCore.*

Data and Ai lead at AWS
Series:  Part 1: AI proposes, people decide  · Part 2 of 2 · Level: 400 (Expert) · Reading time: ~15 minutes

Key terms in this article Lineage edge: a directed link saying "column X in dataset B is derived from column Y in dataset A, by transformation T". OpenLineage: an open standard for lineage events (runs, jobs, datasets, facets). SageMaker Catalog ingests it natively. HITL: human-in-the-loop. A person approves, edits, or rejects machine-proposed edges. Distributed Map: an AWS Step Functions state that runs thousands of child workflows in parallel over a dataset in Amazon S3. ETL: extract, transform, load. JCL / VSAM: mainframe job control language and file storage format. SME: subject matter expert. DLQ: dead-letter queue. MCP: Model Context Protocol, an open standard for exposing tools to AI agents. NL2SQL: natural language to SQL.



Recap: the target design

In  Part 1  I argued that trustworthy lineage needs several independent detectors (runtime capture, code analysis, metadata, data similarity) merged into one graph, under a simple rule: the LLM proposes, evidence and humans decide. Edges that are deterministic or corroborated are accepted automatically. Low-confidence edges, and any edge feeding a regulated report, go to a human with evidence attached.
This part shows how to build that on AWS. Most of it is configuration of managed services. Three components need real engineering: the code-parsing agent, the confidence and review loop, and the agent-facing access layer, and most of this part focuses on those.



Prerequisites

To follow along, you need:
  • An AWS account with permissions to create AWS Step Functions state machines, AWS Lambda functions, Amazon DynamoDB tables, and Amazon S3 buckets
  •  Model access  in Amazon Bedrock for an Anthropic Claude model and an embedding model
  • AWS Glue 5.0 or later for Spark jobs whose lineage you want captured natively
  • Python 3.12 with  boto3  and  sqlglot  packaged into the Lambda deployment (for example, as a Lambda layer)
  • A sample of the legacy SQL, stored procedures, or batch code behind one report, uploaded to an S3 bucket
Cost note: the services in this post incur charges. Run the pilot in a non-production account and follow the  Cleanup  section when you finish.



Solution overview

Figure 1 shows the end-to-end architecture.

Architecture diagram: Glue, EMR, Redshift, mainframe and legacy code sources feed four parallel dete

Figure 1: Detectors A–D run in parallel and emit OpenLineage events. A normaliser merges and scores them, low-confidence edges go to human review, and validated edges land in SageMaker Catalog. Auditors, migration teams, and AI agents all read the same graph.

Layer
AWS services
Build or configure?
Native capture (A)
 AWS Glue 
5.0+,
 Amazon EMR 
,
 Amazon Redshift 
→
 SageMaker Catalog lineage 
Configure
Mainframe analysis (B)
 AWS Transform for mainframe 
Configure + adapter
Code-parsing agent (C)
 AWS Step Functions 
(Distributed Map),
 AWS Lambda 
,
 Amazon Bedrock 
Build
Similarity detector (D)
 AWS Glue Data Quality 
, Amazon Bedrock embeddings,
 Amazon S3 Vectors 
Build (light)
Merge, score, review
Lambda, Step Functions  waitForTaskToken ,
 Amazon Augmented AI (Amazon A2I) 
or a small review app,
 Amazon DynamoDB 
, Amazon S3 Object Lock
Build
System of record
Amazon SageMaker Catalog (lineage API), optionally
 Amazon Neptune 
Configure
Agent access
 Amazon Bedrock AgentCore 
Gateway + AgentCore Policy (Cedar)
Build (thin)






Step 1: Collect the free lineage first

Before writing any AI code, turn on everything AWS captures natively. This gives you a high-confidence baseline that later detectors are checked against.
 Data lineage in SageMaker Unified Studio  is OpenLineage-compatible. It shows lineage captured automatically from supported tools and connections, and lineage you publish yourself through the  PostLineageEvent  API, with column-level tracing in the visualiser.
AWS Glue Spark. OpenLineage libraries are built into AWS Glue 5.0+. Check the limits in the  troubleshooting guide  before relying on this:
Gotcha
Impact
Mitigation
Spark DataFrames only, not Glue DynamicFrames
Older Glue jobs emit nothing
Use DataFrames when you next touch the job; until then, cover it with detector C
Jobs writing to raw S3 paths produce S3 datasets, not Glue tables
Graph nodes don't link to catalog assets
Read and write through Glue tables
300 KB event size limit
Wide tables fail to publish
Tune the column-lineage Spark settings described in the guide
Fine-grained permission mode not supported
No lineage from those jobs
Detector C fallback
Domain in a different Region from the job
Events go nowhere
Set  spark.openlineage.transport.region 
Amazon EMR Spark. EMR's Spark engine also includes the OpenLineage libraries, with minimum versions: EMR Serverless 7.5+, EMR on EC2 7.11+, and EMR on EKS 7.12+. You pass a small set of Spark parameters that point the OpenLineage transport at your domain, and as with Glue, jobs using fine-grained permission mode aren't supported. See  Automate lineage capture from tools  for the parameters, and  Capture data lineage of Amazon EMR Spark jobs into Amazon SageMaker Unified Studio  for a walkthrough.
Redshift. On the Redshift data source connection, tick Import data lineage. SageMaker Catalog then pulls from Redshift system tables daily to derive lineage. No code changes are needed. See  Automate lineage capture from data connections .
Tip: write down coverage before you build anything else, for example "62% of the tables feeding report X have runtime lineage." That number is your baseline, and the gap is the backlog for detectors B–D.



Step 2: Mainframe with AWS Transform

For COBOL/JCL estates, don't build a COBOL parser.  AWS Transform for mainframe  already analyses the codebase. It builds dependency graphs across programs, maps which programs read, write, update, or delete each Db2 table and VSAM file, traces data flow across programs and JCL jobs, and produces a field-level data dictionary from copybooks.
Your job is a small adapter: a Lambda that converts Transform's program↔data-store relationships into OpenLineage  RunEvent s. Each program becomes a job; each Db2 table or VSAM file becomes a dataset with a stable namespace such as  db2://PRODDB2/SCHEMA . Tag these edges  detector=aws-transform  so the scorer can weight them.



Step 3: The code-parsing agent (detector C)

This is where most of the value is, and most of the risk. The target is everything runtime capture can't see: legacy SQL scripts, stored procedures, vendor-ETL exports, Airflow DAGs, PySpark that uses DynamicFrames.

Parse deterministically first, use the LLM only as a fallback

An LLM is the wrong first tool for SQL lineage. It costs more, it isn't deterministic, and it is harder to defend to an auditor. A good open-source parser like  sqlglot  resolves column-level lineage for most well-formed SQL. Send a file to Amazon Bedrock only when:
  • the parser fails (vendor dialect, procedural T-SQL/PL/SQL, dynamic SQL), or
  • the parser succeeds but can't resolve a column ( SELECT * , unknown schema), or
  • you want a plain-language transform summary. This is where the LLM is better than a parser, and reviewers find it very useful.
Path
Typical share of files*
Cost
Determinism
Audit story
sqlglot only
majority
~zero
Full
"Parsed by sqlglot vX.Y"
sqlglot + LLM summary
some
Low
Edges deterministic, summary not
Edges defensible, summary advisory
LLM only
remainder
Medium
Low
Must go to review unless corroborated
*Measure this on your own estate during the pilot. The split varies a lot between a dbt-era warehouse and a 1990s stored-procedure estate.

Fan out with Step Functions Distributed Map

The orchestration is simple: an S3 inventory of the code repository feeds a Distributed Map state, and each item invokes a parse Lambda. Distributed Map runs up to thousands of child executions in parallel, so 2,000 COBOL programs or 100 legacy scripts are a single run, not a project plan.

The parse Lambda

The following sketch shows the core of the parse Lambda. It forces structured output with Bedrock's Converse API and a single required tool. That gives you a schema-validated JSON object instead of free text you then have to parse.
Plain text
import json, os, uuid, datetime
import boto3
import sqlglot
from sqlglot import exp
from sqlglot.lineage import lineage as sg_lineage

s3 = boto3.client("s3")
bedrock = boto3.client("bedrock-runtime")
datazone = boto3.client("datazone")

DOMAIN_ID = os.environ["DATAZONE_DOMAIN_ID"]
MODEL_ID = os.environ["LINEAGE_MODEL_ID"] # e.g. a Claude inference profile ARN
CONFIDENCE_FLOOR = float(os.environ.get("CONFIDENCE_FLOOR", "0.85"))
MAX_EVENT_BYTES = 290_000 # stay under the 300 KB lineage event limit
EVIDENCE_FACET = "myorg_lineageEvidence" # custom facets are prefixed with your producer name

# OpenLineage transformation subtypes, grouped by type
DIRECT_SUBTYPES = {"IDENTITY", "TRANSFORMATION", "AGGREGATION"}
INDIRECT_SUBTYPES = {"JOIN", "GROUP_BY", "FILTER", "SORT", "WINDOW", "CONDITIONAL"}

LINEAGE_TOOL = {
"toolSpec": {
"name": "emit_lineage",
"description": "Emit column-level lineage for the target table defined by this code.",
"inputSchema": {"json": {
"type": "object",
"required": ["target_table", "columns"],
"properties": {
"target_table": {"type": "string"},
"columns": {"type": "array", "minItems": 1, "items": {
"type": "object",
"required": ["target_column", "sources", "subtype",
"summary", "confidence", "evidence_lines"],
"properties": {
"target_column": {"type": "string"},
"sources": {"type": "array", "items": {
"type": "object",
"required": ["table", "column"],
"properties": {"table": {"type": "string"},
"column": {"type": "string"}}}},
"subtype": {"type": "string",
"enum": sorted(DIRECT_SUBTYPES | INDIRECT_SUBTYPES) + ["UNKNOWN"]},
"summary": {"type": "string"},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"evidence_lines": {"type": "array", "items": {"type": "integer"}}
}}}
}}}
}
}

SYSTEM = [{"text": (
"You are a data lineage analyst. Derive column-level lineage ONLY from the code "
"provided. Never invent tables or columns that do not appear in the code. If a "
"source cannot be determined, use subtype UNKNOWN and confidence below 0.5. "
"Cite the line numbers that justify each mapping in evidence_lines.")}]

def table_name(t: exp.Table) -> str:
"""Fully qualified table name without its alias, e.g. 'db.schema.orders'."""
return ".".join(p for p in (t.catalog, t.db, t.name) if p)

def infer_subtype(expression: exp.Expression) -> str:
"""Classify a projected expression into an OpenLineage subtype."""
e = expression.unalias() if isinstance(expression, exp.Alias) else expression
if isinstance(e, exp.Column):
return "IDENTITY"
if e.find(exp.AggFunc):
return "AGGREGATION"
if e.find(exp.Case, exp.If):
return "CONDITIONAL"
return "TRANSFORMATION"

def try_sqlglot(sql: str, dialect: str, target_cols: list[str], schema: dict | None):
"""Deterministic path. Returns column mappings, or None if anything is unresolved."""
if not target_cols:
return None
mappings = []
try:
for col in target_cols:
node = sg_lineage(col, sql, schema=schema, dialect=dialect)
sources = [{"table": table_name(leaf.source), "column": leaf.name.split(".")[-1]}
for leaf in node.walk()
if not leaf.downstream and isinstance(leaf.source, exp.Table)]
if not sources:
return None
mappings.append({
"target_column": col, "sources": sources,
"subtype": infer_subtype(node.expression),
"summary": "", "confidence": 0.98, "evidence_lines": [],
# deterministic evidence: the exact expression the parser resolved
"evidence_expr": node.expression.sql(dialect=dialect)[:500],
})
return mappings
except Exception:
return None

def ask_bedrock(code: str, hint: str) -> dict:
numbered = "\n".join(f"{i+1:>5}: {l}" for i, l in enumerate(code.splitlines()))
resp = bedrock.converse(
modelId=MODEL_ID,
system=SYSTEM,
messages=[{"role": "user", "content": [{"text": f"{hint}\n\n<code>\n{numbered}\n</code>"}]}],
toolConfig={"tools": [LINEAGE_TOOL],
"toolChoice": {"tool": {"name": "emit_lineage"}}},
inferenceConfig={"temperature": 0},
)
for block in resp["output"]["message"]["content"]:
if "toolUse" in block:
return block["toolUse"]["input"]
raise ValueError("Model did not return structured lineage")

def transformation(c: dict) -> dict:
sub = c["subtype"]
if sub in DIRECT_SUBTYPES:
t = {"type": "DIRECT", "subtype": sub}
elif sub in INDIRECT_SUBTYPES:
t = {"type": "INDIRECT", "subtype": sub}
else: # UNKNOWN: keep the edge, omit the subtype, and force review downstream
t = {"type": "INDIRECT"}
if c.get("summary"):
t["description"] = c["summary"][:500]
return t

def to_openlineage(columns: list[dict], target_table: str, job_name: str,
namespace: str, evidence: dict) -> dict:
"""Build an OpenLineage RunEvent with a columnLineage facet."""
fields, inputs = {}, {}
for c in columns:
fields[c["target_column"]] = {"inputFields": [
{"namespace": namespace, "name": s["table"], "field": s["column"],
"transformations": [transformation(c)]} for s in c["sources"]]}
for s in c["sources"]:
inputs[s["table"]] = {"namespace": namespace, "name": s["table"]}
producer = f"https://example.com/lineage-detectors/{evidence['detector']}"
return {
"eventType": "COMPLETE",
"eventTime": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"producer": producer,
"schemaURL": "https://openlineage.io/spec/2-0-2/OpenLineage.json#/definitions/RunEvent",
"run": {"runId": str(uuid.uuid4()),
"facets": {EVIDENCE_FACET: {"_producer": producer,
"_schemaURL": "https://example.com/schemas/evidence.json",
**evidence}}},
"job": {"namespace": namespace, "name": job_name},
"inputs": list(inputs.values()),
"outputs": [{"namespace": namespace, "name": target_table, "facets": {"columnLineage": {
"_producer": producer,
"_schemaURL": "https://openlineage.io/spec/facets/1-2-0/ColumnLineageDatasetFacet.json",
"fields": fields}}}],
}

def handler(event, _ctx):
"""Invoked per file by Distributed Map. Returns scored results; publishing happens
downstream so that low-confidence results can be routed to review first."""
code = s3.get_object(Bucket=event["bucket"], Key=event["key"])["Body"].read().decode()
target_table = event.get("target_table")
mappings = None
if target_table: # the deterministic path needs to know which table it is resolving
mappings = try_sqlglot(code, event.get("dialect", "tsql"),
event.get("target_columns", []), event.get("schema"))
if mappings:
columns, detector = mappings, "sqlglot"
evidence_meta = {"parser_version": sqlglot.__version__}
else:
result = ask_bedrock(code, f"Dialect hint: {event.get('dialect', 'unknown')}")
columns, detector = result.get("columns") or [], "bedrock-claude"
target_table = target_table or result.get("target_table")
evidence_meta = {"model_id": MODEL_ID}

if not columns or not target_table:
return {"events": [], "detector": detector, "min_confidence": 0.0,
"needs_review": True, "reason": "no lineage extracted", "key": event["key"]}

min_conf = min(c["confidence"] for c in columns)
has_unknown = any(c["subtype"] == "UNKNOWN" for c in columns)
evidence = {"detector": detector, "source_uri": f"s3://{event['bucket']}/{event['key']}",
"min_confidence": min_conf, **evidence_meta,
"columns": {c["target_column"]: {"lines": c["evidence_lines"],
"expr": c.get("evidence_expr", "")}
for c in columns}}
events = split_to_fit(columns, target_table, event["key"], event["namespace"], evidence)
return {"events": events, "detector": detector, "min_confidence": min_conf,
"needs_review": detector != "sqlglot" and (min_conf < CONFIDENCE_FLOOR or has_unknown)}

def split_to_fit(columns, target_table, job_name, namespace, evidence):
"""Split wide tables into several events so each stays under the size limit."""
ev = to_openlineage(columns, target_table, job_name, namespace, evidence)
if len(json.dumps(ev).encode("utf-8")) <= MAX_EVENT_BYTES or len(columns) == 1:
return [ev]
mid = len(columns) // 2
trim = lambda cols: {**evidence, "columns": {c["target_column"]: evidence["columns"][c["target_column"]]
for c in cols}}
return (split_to_fit(columns[:mid], target_table, job_name, namespace, trim(columns[:mid])) +
split_to_fit(columns[mid:], target_table, job_name, namespace, trim(columns[mid:])))

def publish(ol_event: dict):
"""Called only for auto-approved or human-approved edges."""
payload = json.dumps(ol_event).encode("utf-8")
if len(payload) > MAX_EVENT_BYTES:
raise ValueError(f"Lineage event is {len(payload)} bytes; split it before publishing")
datazone.post_lineage_event(domainIdentifier=DOMAIN_ID, event=payload,
clientToken=ol_event["run"]["runId"])
Note: this is a starting skeleton, not production code. Add retries, chunking for very large files, cross-file schema context, and IAM least privilege. The publishing role needs  datazone:PostLineageEvent  on the domain; AWS KMS permissions too if the domain uses a customer managed key.
Three design choices here matter more than the code itself:
  1. Evidence on every edge. LLM-derived edges cite numbered  evidence_lines ; deterministic edges record the parser version and the exact expression sqlglot resolved. Every edge points to what justifies it. Reviewers confirm the cited lines, and auditors get a citation for every edge.
  1.  temperature: 0  and a forced tool. You want the same answer on re-runs and a schema-valid object every time.
  1. Publishing is separate from parsing. The parse step never writes to the system of record. Only the review workflow does, and  split_to_fit  keeps each event under the 300 KB limit.



Step 4: The similarity detector (detector D)

Some lineage has no code at all: a CSV someone exports every month, a table copied by a vendor tool, a column renamed in a migration. To find these you compare the data itself.
For each column, build a compact profile from AWS Glue Data Quality statistics (type, null %, distinct count, min/max, top values), plus the column name and catalog description. Embed the profile as text with an Amazon Bedrock embedding model, store the vectors in Amazon S3 Vectors, and run k-nearest-neighbour search across systems.
Treat every similarity match as a suggestion.  customer_dob  in two systems will look alike whether or not one feeds the other. These edges always go to review, and they're labelled  CANDIDATE , not  DERIVED , in the graph.



Step 5: Merge, score, review

Every detector emits OpenLineage, so merging becomes an identity problem rather than a format problem. A normaliser Lambda canonicalises namespaces and dataset names. It uses the  sourceIdentifier  attribute so that published nodes link to the assets already in SageMaker Catalog (see  The importance of the sourceIdentifier attribute to lineage nodes ). For AWS resources,  sourceIdentifier  is an ARN, such as  arn:aws:glue:<region>:<account-id>:table/<database>/<table-name> , so the normaliser should map legacy names to these ARNs wherever a catalogued asset exists.).

A simple, explainable confidence policy

This is the routing policy from  Part 1 , expressed as detector rules. Don't over-engineer the scorer. Auditors and reviewers need to understand it.
Evidence for an edge
Route
Runtime capture (A) or AWS Transform (B)
Auto-approve
sqlglot (C, deterministic)
Auto-approve
LLM (C) and corroborated by A/B or by sqlglot on a sibling file
Auto-approve
LLM (C) alone, confidence ≥ floor, low-criticality domain
Auto-approve, sampled 10% for review
LLM (C) alone, below floor or feeds a regulated report
Human review
Similarity (D)
Human review (always)
Detectors disagree
Human review (shown as a conflict)

The review workflow

Step Functions'  .waitForTaskToken  integration pauses the workflow for each edge (or, better, each target table) until a reviewer acts. The task token goes onto the review queue. The review UI, whether Amazon A2I or a small internal app, shows:
  • the proposed edge and its transformation summary,
  • the evidence: highlighted code lines, profile comparison, or the conflicting edge,
  • Approve / Edit / Reject buttons that call  SendTaskSuccess  with the decision.
On approval, the workflow calls  publish() . Every decision is written to DynamoDB, and the evidence bundle to S3 with Object Lock: detector, model ID, prompt version, reviewer identity, timestamp. This record is what makes the lineage "audit-grade" rather than just plausible.
Rejections are useful training data. Every rejected or edited LLM edge becomes a labelled example. Track precision and recall per detector and per code dialect, and use the failures to improve prompts. Without this you have no evidence when someone asks how accurate the system is.



Step 6: One graph, three audiences

Compliance and audit use the lineage view in SageMaker Unified Studio to trace a report column back to source, and an  Amazon Quick Sight  dashboard over the DynamoDB decisions table for coverage and review metrics.
Migration teams query source-to-target mappings and downstream impact before each cut-over wave. If you need deep multi-hop traversals, such as "everything within six hops of this Db2 table", sync the validated graph one way into Amazon Neptune and use openCypher. The  Glue + Neptune + Spline lineage pattern  is a good reference for the graph model. Keep SageMaker Catalog as the single system of record; Neptune is a read-optimised projection.
AI agents are where lineage moves from being a cost to being an enabler. Expose a small set of lineage tools through Amazon Bedrock AgentCore Gateway as MCP tools:
Tool
Backed by
Agent uses it to…
 get_upstream(dataset, column) 
 GetLineageNode  traversal
Check where a metric really comes from before using it
 is_validated(dataset) 
DynamoDB decisions table
Refuse to query datasets with unreviewed lineage
 find_authoritative(business_term) 
Catalog glossary + lineage
Choose the golden source, not a stale copy
Then use AgentCore Policy (Cedar) to make validation a hard rule, not a suggestion. For example, an NL2SQL agent's query tool is only permitted against datasets whose lineage status is  VALIDATED . This is the same deterministic, policy-enforced pattern used for governed agentic data access: the agent can propose which data to use, and the policy decides.



Well-Architected checklist

Pillar
What to do
Security
Scope  PostLineageEvent  to the publish role only; reader roles get  GetLineageNode  /  ListLineageNodeHistory . Use an AWS Key Management Service (AWS KMS) customer managed key on the domain. VPC endpoints for DataZone and Bedrock so code never crosses the internet. Bedrock doesn't use your prompts or code to train models.
Reliability
Distributed Map with per-item retries and a failure threshold; failed items go to an Amazon Simple Queue Service (Amazon SQS) DLQ for re-runs. Use  clientToken  on publish so retries are idempotent.
Operational excellence
Everything in the AWS Cloud Development Kit (AWS CDK). Amazon EventBridge rules on code-repo commits and new Glue/Redshift jobs trigger incremental re-parsing, which handles drift. Amazon CloudWatch alarms when a runtime event contradicts a validated edge.
Cost optimisation
Deterministic parse first. Use Bedrock batch inference for the initial backlog; on-demand only for incremental changes. Profile only the columns in the target domain, not the whole estate.
Performance efficiency
Chunk very large files by procedure or paragraph. Add Neptune only when traversal depth needs it.
Sustainability
Incremental re-parsing only. Don't re-analyse unchanged code.



Cleanup

To avoid ongoing charges, delete the resources you created:
  1. Stop or delete the Step Functions state machine and any running executions, then delete the parse and normaliser Lambda functions.
  1. Delete the DynamoDB decisions table and the S3 Vectors index used by the similarity detector.
  1. Delete the S3 buckets for code inventory and evidence. Buckets with S3 Object Lock in compliance mode can't be emptied until the retention period ends, so use a short retention period for pilots.
  1. If you created one, delete the Neptune cluster.
  1. In SageMaker Unified Studio, turn off Import data lineage on your Redshift connections, and delete test lineage nodes if you don't want them kept.
  1. Remove the AgentCore Gateway targets and policies you created for the lineage tools.



Conclusion

In this post, you built AI-assisted, human-validated lineage on AWS. Native capture in SageMaker Catalog and AWS Transform provides the high-confidence baseline. A Step Functions and Amazon Bedrock code-parsing agent fills the gaps, with every edge citing its evidence. A confidence policy and a review workflow decide what becomes fact, and AgentCore Policy lets agents use only validated data.
Don't deploy this across the whole estate on day one. Follow the six-week pilot from  Part 1 : one regulatory report or migration wave, native capture first, detector C on the code behind it, expert review against a manual sample, then one consumer connected. The most valuable result is a precision and recall figure for your own code, because it turns lineage from a vendor claim into a control you can defend.
Try it on one report and share your precision numbers in the comments. I'd like to know which code dialects gave the parser and the model the most trouble.



References

Opinions are my own. Code samples are illustrative and provided as-is; review and test before production use.

Opinions are my own. Code samples are illustrative and provided as-is; review and test before production use.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article