AWS Lesson 105 of 123

AWS Enterprise Architecture: Data Mesh

In a nutshell

Imagine a city that outgrew its one central warehouse. Every shop’s inventory was trucked to that single building, one crew logged and shelved all of it, and anyone who wanted stock filed a request and waited in a queue that never got shorter. A data mesh replaces that warehouse with a farmers’ market: each grower (a business domain — Sales, Finance, Supply Chain) runs their own stall, packages their own goods as clearly-labelled products, and sells directly to buyers. A small market office grows and stores nothing — it sets the rules everyone follows (labelling, weights, who may buy age-restricted goods) and hands out the passes that open specific stalls. Growers own the goods; the office owns the rules and the audit log.

On AWS that market office is a central governance account running AWS Lake Formation; each stall is a producer account with its own S3 storage and Glue Data Catalog; the labels are LF-Tags (domain=sales, sensitivity=pii); the passes are Lake Formation grants that vend short-lived credentials so a buyer’s Athena query reads the grower’s data in place, filtered down to just the rows and columns they’re allowed to see. Nothing is copied, no single team is a bottleneck, and every access is logged in one place.

Why a beginner should care: “data mesh” sounds like a product you buy, but it’s really an operating model wired together from primitives you may already know — Organizations, S3, Glue, IAM, and Lake Formation. Learn how those five snap together here and you can reason about almost any large-company data platform, and read a real one’s architecture diagram without getting lost.

Level: Advanced · Time: ~50 min

Prerequisites — you’ll move faster if you’ve met these first:

After this lesson you can:

A centralized data lake works right up until it doesn’t. One team owns the pipelines, every other team files a ticket, and the backlog grows faster than the data. A data mesh flips the ownership model: the teams that produce the data also own it as a product, publish it through a shared governance layer, and let other domains discover and consume it without a central bottleneck. On AWS the load-bearing primitives for this are AWS Lake Formation (fine-grained, cross-account permissions), the AWS Glue Data Catalog (the technical metadata backbone), and per-domain AWS accounts wired together under AWS Organizations. This article is a reusable reference for standing that up — from a three-domain startup to a fifty-domain enterprise.

The business scenario

Picture a mid-market retailer, growing fast, that has accumulated the classic “data gravity” problem. Sales, Marketing, Supply Chain, and Finance each generate operational data in their own systems. Eighteen months ago a small platform team built a central data lake in a single AWS account and offered to ingest everyone’s data. It worked beautifully for the first three pipelines. Now it’s a chokepoint:

This is the moment a data mesh pays for itself. The four principles — domain ownership, data as a product, self-serve platform, and federated computational governance — map almost one-to-one onto AWS building blocks. The goal isn’t “more technology”; it’s to decentralize the production of data while centralizing the governance of access. Crucially, this is not a big-bang rewrite: the same pattern that serves a 3-domain company scales to 50 domains by adding accounts, not by re-architecting.

What “good” looks like at the end:

Architecture overview

The end-to-end shape is a hub-and-spoke catalog with in-place, cross-account data sharing. Storage and compute live in the spokes (domains); governance and the source-of-truth catalog live in the hub.

AWS data mesh reference architecture: producer domain accounts register S3 data products in the Glue Data Catalog and apply LF-Tags, share their catalog to a central governance account via AWS RAM and Lake Formation, and consumer analytics accounts query the data in place with Athena and Redshift Spectrum through resource links under Lake Formation-vended, column- and row-filtered credentials.

Accounts (the spokes and the hub). Under a single AWS Organization, you create one OU per data concern. A Governance OU holds the central governance account. A Domains OU holds one producer account per domain (Sales, Marketing, Supply Chain, Finance, …). Optionally a Consumers OU holds analytics/BI accounts for teams that consume but don’t produce. AWS Organizations + Service Control Policies (SCPs) provide the guardrails; AWS RAM (Resource Access Manager) is the wire that Lake Formation uses to share catalog resources across these account boundaries.

Producer (domain) path — how data becomes a product. Inside a domain account, source data lands in a domain-owned S3 bucket (e.g. s3://acme-sales-dataproducts/). An AWS Glue crawler or an explicit Glue ETL/CREATE TABLE job registers the schema into that account’s Glue Data Catalog, and the underlying S3 location is registered as a Lake Formation data lake location so that Lake Formation — not raw S3 IAM — mediates access. The domain team curates the table: partitions, a schema contract, and LF-Tags (Lake Formation tag-based access control attributes) such as domain=sales, sensitivity=pii, layer=curated. At this point the table is a candidate data product.

Governance path — the hub takes the source of truth. The recommended pattern (AWS calls it the centralized catalog / central governance account model) is that the producer shares its database/tables to the central governance account via Lake Formation cross-account grants. The central account becomes the authoritative catalog: it owns the LF-Tag taxonomy, holds the resource links, and is where all consumer-facing grants are issued. This gives you one place to define “who can see PII,” one place to audit, and one tag ontology for the whole company — federated governance, decentralized ownership.

Consumer path — discover, request, query in place. A consumer (say, a Marketing analyst in the analytics account) browses available products in a data catalog/portal — Amazon DataZone (now folded into the next-generation SageMaker Catalog / Amazon SageMaker Unified Studio) is the AWS-native option, or a lightweight internal portal backed by Glue Catalog APIs. They submit an access request. Governance approves it by issuing a Lake Formation grant — ideally an LF-Tag policy (“grant SELECT on all tables where domain=sales AND sensitivity=public to the Marketing analytics role”) rather than a per-table grant. AWS RAM propagates the share; the consumer account creates a resource link to the shared database, and the analyst runs Athena (or Redshift Spectrum, or EMR/Spark) directly against the producer’s S3 data. Lake Formation enforces column-level, row-level, and cell-level filters at query time, and the data is never copied — the analyst’s compute reads the producer’s bucket through the Lake Formation credential vending path.

So the request/data path for a single query is: analyst → Athena in consumer account → resource link → Lake Formation (central grant + LF-Tag policy + data-filter) → vended temporary credentials → producer’s registered S3 location → filtered result back to Athena. Metadata flows hub-and-spoke; bytes flow point-to-point from producer storage to consumer compute, governed end to end.

Component breakdown

Component What it does Why it’s here Key configuration choices
AWS Organizations + OUs Account hierarchy and policy inheritance Hard account boundaries give per-domain isolation, blast-radius control, and clean cost attribution OUs: Governance, Domains, Consumers, Security/Log-archive. Enable trusted access for Lake Formation, RAM, and CloudTrail org trail
Service Control Policies (SCPs) Org-wide guardrails Prevent domains from disabling encryption, leaving the org, or creating public S3 — governance you can’t opt out of Deny s3:PutBucketPublicAccessBlock removal, deny disabling default encryption, restrict regions, protect Lake Formation settings
Domain S3 buckets Physical storage of each domain’s data products Producers own their bytes; storage lives where the domain lives Bucket-per-domain (or per-layer: raw/curated/product). SSE-KMS with a domain CMK; register the bucket/prefix as a Lake Formation location
AWS Glue Data Catalog Technical metadata (databases, tables, schemas, partitions) The lingua franca every engine (Athena, Redshift, EMR, Spark) reads; the thing that gets shared across accounts One catalog per account; per-database settings. Use Glue 4.0+ crawlers or explicit DDL; enable partition indexing for large tables
AWS Lake Formation Fine-grained permissions + cross-account sharing Replaces unmanageable per-table IAM with column/row/cell-level grants and tag policies; vends scoped credentials Switch databases to Lake Formation permissions (remove IAMAllowedPrincipals). Define LF-Tags. Use Version 3+ cross-account grants. Set up data filters for row/column security
LF-Tags (TBAC) Attribute taxonomy on catalog resources Grant on attributes (sensitivity, domain, layer) instead of on hundreds of individual tables — this is what makes the mesh scale Centralize tag definitions in the governance account; delegate tag assignment to domains for their own resources
AWS RAM Shares catalog resources across accounts The transport Lake Formation uses for cross-account grants Enable sharing within the org; accept shares (or auto-accept for trusted org); creates the cross-account principal plumbing
Resource links Account-local pointer to a shared database/table Lets consumers query a remote catalog object as if it were local Create in the consumer account after a share is accepted; point Athena/Redshift at the link
Athena / Redshift Spectrum / EMR Query and compute engines in consumer accounts In-place query — compute is decentralized and billed to the consumer Athena workgroups per team with result-location + cost controls; Redshift via Spectrum or data sharing; EMR/Spark with Lake Formation integration enabled
Amazon DataZone / SageMaker Catalog Business catalog, data portal, subscription workflow Human-facing discovery, glossaries, and a request/approve flow on top of the technical catalog Map DataZone domains/projects to AWS accounts; let subscriptions drive the underlying Lake Formation grants
CloudTrail + Lake Formation access logs + CloudWatch Audit and observability Prove who accessed what, across every account, from one place Org-level CloudTrail to a central log-archive account; Lake Formation audit events; per-domain cost & query dashboards

A few of these deserve emphasis. LF-Tags are the single most important scaling decision. Without them, every new consumer means a fresh round of per-table grants and your governance team becomes the new bottleneck — you’ve just moved the ticket queue. With them, you grant once against an attribute (SELECT where layer=curated AND sensitivity=public) and every current and future table that carries those tags is automatically in scope. Data filters (row-level expressions and column projections, including cell-level via combining both) are how a single shared table serves both a Finance user who may see salaries and a regional manager who may see only their region’s rows.

Implementation guidance

Bootstrapping the org and accounts. Use AWS Control Tower to lay down the landing zone (OUs, guardrails, centralized logging, an account factory). Provision domain accounts through Account Factory for Terraform (AFT) so every new domain comes pre-baked with the same baseline. The data-mesh-specific wiring is best expressed as Terraform and applied per account from a CI/CD pipeline (CodePipeline or GitHub Actions assuming an OIDC role per account):

Networking. The catalog/RAM control plane is account-to-account over AWS’s backbone — no VPC needed for the sharing. For the data plane, keep S3 and analytics traffic private: use S3 Gateway VPC Endpoints in each consumer VPC, Interface (PrivateLink) Endpoints for Glue, Lake Formation, Athena, and KMS, and avoid routing analytics traffic over the public internet. If consumers use Redshift or EMR in private subnets, this keeps the entire query path inside your network perimeter. Cross-account KMS access must be granted in the key policy of each domain’s CMK so consumer roles can decrypt the data they’re authorized to read.

Identity wiring. Standardize on IAM Identity Center (SSO) for human access, mapping enterprise groups (e.g. marketing-analysts) to permission sets that assume the right roles in consumer accounts. The role that actually queries data is registered as a Lake Formation principal and is what grants target. For machine/pipeline access inside domains, use account-scoped IAM roles assumed via OIDC from CI. The chain to internalize: Identity Center group → permission set → consumer-account role → Lake Formation grant (via LF-Tag policy) → data filter → vended S3+KMS credentials. Lake Formation does the last-mile authorization; IAM only gets the principal to the door.

Producer onboarding flow (the self-serve part). A domain team should be able to publish a product without a governance ticket: (1) drop curated data in their registered bucket, (2) run the crawler / apply the table contract, (3) assign LF-Tags they’re delegated to manage, (4) share to the central governance account (a templated Terraform module), and (5) register the product in DataZone. Governance only intervenes to approve cross-domain consumption, and even that can be policy-driven for low-sensitivity tiers.

Worked example: onboarding the Sales domain end to end

The sections above describe the moving parts; here is the actual wiring, step by step, for one producer domain (Sales, account 111111111111) publishing a product that one consumer (Analytics, account 333333333333) queries, through the central governance account (222222222222). Every ARN and account ID is a placeholder — swap in your own. Nothing is run here; the snippets are schema-correct for the aws Terraform provider v5 and the current aws CLI, and any sample output is representative.

Step 0 — one-time governance setup (governance account 222222222222)

Make Lake Formation — not legacy IAM — the authority, and define the tag taxonomy once for the whole company.

# Provider is aliased per account; this block targets the governance account.
resource "aws_lakeformation_data_lake_settings" "governance" {
  admins = [aws_iam_role.lf_admin.arn]

  # Turn OFF the legacy default: no blanket grant to IAMAllowedPrincipals.
  # Empty blocks = new databases/tables are governed by Lake Formation only.
  create_database_default_permissions {}
  create_table_default_permissions {}
}

# The tag taxonomy — defined centrally, this is the backbone of the mesh.
resource "aws_lakeformation_lf_tag" "sensitivity" {
  key    = "sensitivity"
  values = ["public", "internal", "confidential", "pii"]
}
resource "aws_lakeformation_lf_tag" "domain" {
  key    = "domain"
  values = ["sales", "marketing", "supplychain", "finance"]
}
resource "aws_lakeformation_lf_tag" "layer" {
  key    = "layer"
  values = ["raw", "curated", "product"]
}

The empty create_database_default_permissions {} / create_table_default_permissions {} blocks are the Terraform way to say “grant nothing to IAMAllowedPrincipals by default.” Omit them and you keep the legacy ALL-to-everyone behaviour — the single most common mesh misconfiguration, and the one that silently defeats every grant you write below.

Step 1 — register storage and catalog (Sales account 111111111111)

resource "aws_s3_bucket" "sales" {
  bucket = "acme-sales-dataproducts"
}

# Hand the S3 location to Lake Formation via a registration role that can read it.
resource "aws_lakeformation_resource" "sales_bucket" {
  arn      = aws_s3_bucket.sales.arn          # the location, no /* suffix
  role_arn = aws_iam_role.lf_register.arn     # the credential-vending role
}

resource "aws_glue_catalog_database" "sales_curated" {
  name = "sales_curated"
}

# A crawler infers the schema and writes tables into the catalog.
resource "aws_glue_crawler" "orders" {
  name          = "sales-orders-crawler"
  role          = aws_iam_role.glue.arn
  database_name = aws_glue_catalog_database.sales_curated.name
  schedule      = "cron(0 3 * * ? *)"          # nightly at 03:00 UTC
  s3_target {
    path = "s3://acme-sales-dataproducts/curated/orders/"
  }
}

You register the location, not the objects — the ARN has no /*. From now on, reads of anything under that prefix are mediated by Lake Formation and the vending role, not by raw S3 bucket policies.

Step 2 — tag the product (Sales account)

Tags are what you grant on later, so this step is where a table becomes a governable product. The domain team assigns tags to its own resources because governance delegated them that right.

resource "aws_lakeformation_resource_lf_tags" "orders_table" {
  table {
    database_name = aws_glue_catalog_database.sales_curated.name
    name          = "orders"
  }
  lf_tag {
    key   = "domain"
    value = "sales"
  }
  lf_tag {
    key   = "layer"
    value = "curated"
  }
  lf_tag {
    key   = "sensitivity"
    value = "internal"
  }
}

LF-Tags inherit down the hierarchy: a tag on the database is seen by every table and column beneath it unless a lower level overrides it. Tag the database domain=sales once and every future table inherits it — no per-table busywork, which is exactly what makes the taxonomy cheap to run.

Step 3 — share to governance, then govern (cross-account grant)

In the centralized-catalog model the producer grants its tagged resources up to the governance account with grant option, so governance can re-issue access to consumers:

# In the Sales account: share the "sales" slice up to governance (222222222222).
resource "aws_lakeformation_permissions" "share_to_governance" {
  principal                     = "222222222222"
  permissions                   = ["SELECT", "DESCRIBE"]
  permissions_with_grant_option = ["SELECT", "DESCRIBE"]

  lf_tag_policy {
    resource_type = "TABLE"
    expression {
      key    = "domain"
      values = ["sales"]
    }
  }
}

permissions_with_grant_option is the crucial half: without it, governance could read Sales data but could not re-grant it to Analytics. Lake Formation (cross-account share version 3+) creates the underlying AWS RAM share automatically; if your org has RAM sharing enabled with auto-accept, there is no manual acceptance step.

Step 4 — the one grant that scales (governance account)

Instead of hundreds of per-table grants, governance issues one tag-policy grant per consumer role. This is the line that makes the mesh scale.

aws lakeformation grant-permissions \
  --principal DataLakePrincipalIdentifier=arn:aws:iam::333333333333:role/analytics-query \
  --permissions SELECT DESCRIBE \
  --resource '{
    "LFTagPolicy": {
      "ResourceType": "TABLE",
      "Expression": [
        {"TagKey": "sensitivity", "TagValues": ["public", "internal"]}
      ]
    }
  }'

Read it as: “the analytics-query role in account 333333333333 may SELECT any table tagged sensitivity ∈ {public, internal}, in any domain, now or in the future.” Onboard a tenth domain next quarter and its internal tables are already in scope — zero new grants.

Step 5 — consumer creates a resource link and queries (Analytics 333333333333)

A shared database is remote; a resource link is the local pointer Athena can see.

aws glue create-database --database-input '{
  "Name": "sales_curated_link",
  "TargetDatabase": {
    "CatalogId": "222222222222",
    "DatabaseName": "sales_curated"
  }
}'

Then the analyst simply queries — compute is billed to Analytics, the bytes stay in the Sales bucket:

SELECT order_date, category, SUM(amount) AS revenue
FROM   sales_curated_link.orders
WHERE  order_date >= DATE '2026-01-01'
GROUP  BY order_date, category;

Lake Formation intercepts the plan, checks the grant, applies any data filter, vends temporary S3+KMS credentials scoped to exactly the allowed columns/rows, and streams the filtered result back. The Analytics account never held a bucket policy on the Sales bucket.

Step 6 — column and row security with a data filter

Finance’s gl_transactions must hide employee_salary from analysts and show a regional controller only their region. That is one data cells filter:

aws lakeformation create-data-cells-filter --table-data '{
  "TableCatalogId": "222222222222",
  "DatabaseName":   "finance_curated",
  "TableName":      "gl_transactions",
  "Name":           "apac_controller_no_salary",
  "RowFilter":      { "FilterExpression": "region_code = '\''APAC'\''" },
  "ColumnWildcard": { "ExcludedColumnNames": ["employee_salary"] }
}'

Column projection (ColumnWildcard.ExcludedColumnNames) hides the PII column; the row FilterExpression is a static WHERE-style predicate. Combine both and you get cell-level security — the same physical table safely serves a payroll admin and a regional manager. Two things people miss: you grant the filter (not the base table) to the restricted role, and row expressions are static — there is no “current user’s region” function, so per-region isolation is modelled as one filter per region, each granted to the matching controller role (or you push identity into IAM Identity Center session tags and separate roles).

Step 7 — the data contract (both accounts agree on this)

A data product is more than a table; publish a machine-readable contract next to it so consumers know what they’re subscribing to and breaking changes become reviewable events rather than 3 a.m. surprises:

product: sales.orders
owner: sales-data@acme.example
domain: sales
sensitivity: internal
sla:
  freshness: "<= 1h"
  availability: "99.9%"
schema:
  - name: order_id
    type: string
    nullable: false
  - name: order_date
    type: date
    nullable: false
  - name: amount
    type: "decimal(12,2)"
    nullable: false
  - name: category
    type: string
    nullable: true
version: 2
breaking_change_policy: "major version bump + 30-day deprecation"

Keep it in the domain repo beside the Terraform. A CI check can diff the live Glue schema against this contract and fail the pipeline on an unversioned breaking change — governance enforced as code, not as a meeting.

Enterprise considerations

Security & Zero Trust. The mesh is a Zero-Trust data architecture by construction: no principal has standing access to a bucket; every read is an explicit, attribute-based grant evaluated at query time, with temporary vended credentials rather than long-lived bucket policies. Enforce least privilege with LF-Tag policies scoped to the minimum sensitivity tier, column-level security to hide PII columns from analysts who don’t need them, and row-level filters for tenant/region isolation. SCPs make the non-negotiables (encryption, public-access-block, region pinning) un-bypassable. Always remove IAMAllowedPrincipals — leaving it on silently bypasses Lake Formation and is the single most common misconfiguration. Encrypt with per-domain KMS CMKs so a domain can cryptographically revoke access, and audit decrypt usage via CloudTrail.

Cost optimization. Decentralization is itself a cost lever: each domain’s storage and each consumer’s compute hit their own bill, so showback/chargeback is automatic and teams feel their Athena scans. Concretely: store products in Parquet, partitioned and compacted, so Athena/Spectrum scan less; use Glue partition indexes to cut partition-filtering cost; set Athena per-query and per-workgroup data-scanned limits; lifecycle raw data to S3 Intelligent-Tiering / Glacier; and prefer in-place query over copy to avoid duplicating petabytes across accounts. Lake Formation and RAM themselves carry no per-request fee — you pay for Glue, S3, KMS, and the query engines.

Scalability. This architecture scales by adding accounts and tags, not by re-architecting. Going from 5 to 50 domains is 45 more applications of the same Terraform baseline. The LF-Tag taxonomy means grant complexity grows with the number of attributes (a handful) rather than the number of tables (thousands). Watch the real limits: Glue/Lake Formation have account- and region-level quotas on databases, tables, partitions, and concurrent grants — design partitioning to stay well under partition limits, and federate very large domains across multiple accounts if needed.

Reliability & DR (RTO/RPO). S3 gives 11-nines durability per region; for regional resilience, enable S3 Cross-Region Replication on product buckets and replicate the Glue Data Catalog (export/replicate databases and tables, or rebuild via crawlers from replicated data) and the Lake Formation grants (they’re Terraform — re-apply in the DR region). A practical posture: RPO ≈ 15 min for product data via CRR, RTO ≈ 1–2 hours to re-point catalog and re-issue grants in the secondary region, since compute (Athena/EMR) is stateless and stands up quickly. The catalog and grant definitions living in version-controlled IaC is what makes a fast RTO realistic — your “DR plan” is largely terraform apply in another region.

Observability. Centralize an organization CloudTrail in a log-archive account so every cross-account share and data access is captured in one place. Use Lake Formation’s access logging to answer “who queried this PII table last quarter.” Per-domain CloudWatch dashboards track pipeline health and freshness against the product’s SLA; per-consumer Athena dashboards track bytes scanned and cost. Surface data product health (freshness, completeness, last-updated) in the DataZone portal so consumers trust what they subscribe to.

Governance. This is federated computational governance: standards (tag taxonomy, encryption, naming, PII classification) are defined centrally and enforced as code (SCPs, Lake Formation policies, IaC modules), while domains retain autonomy over modeling and publishing. The governance account is the policy authority and the audit point — not a data owner. Define a lightweight data contract per product (schema, SLA, owner, sensitivity) and make breaking-schema-changes a versioned, reviewed event rather than a silent crawler update.

Reference enterprise example

NimbusCart, a mid-market online retailer (~1,200 employees, ₹1,800 crore revenue), ran the exact centralized-lake bottleneck described above: one platform team, a 9-week backlog, and full-bucket access in a shared analytics account. They migrated to a data mesh over two quarters.

What they built. Under a Control Tower landing zone, they created a Governance OU (one central governance account) and a Domains OU with four producer accounts: Sales, Marketing, Supply Chain, and Finance. A separate Consumers OU held a Analytics account (BI + ad-hoc) and a Data Science account.

The numbers.

A decision they got right. Early on, an engineer proposed per-table grants “to keep it simple for the first three tables.” Governance overruled it and mandated LF-Tags from day one. When the company later onboarded its 5th and 6th domains and tripled its consumers, no new grants were needed for public/internal data — the new tables simply inherited the existing tag policies. That single choice is the difference between a mesh that scales and a mesh that becomes the new bottleneck.

The outcome. Six months in, the central platform team of four had repositioned from “pipeline operators” to “platform + governance,” each domain owned its products end-to-end, PII exposure went from “everyone in the analytics account” to “explicitly granted, audited, and mostly nobody,” and the 9-week backlog was gone because the backlog’s owner — the central team — was no longer in the critical path.

When to use it

Use a data mesh on AWS when you have multiple independent data-producing domains with their own teams, a central lake that has become a bottleneck, and a real need for fine-grained, auditable cross-team data sharing (especially with PII/regulatory pressure). It shines when domains genuinely understand their data better than any central team could, and when organizational ownership can actually be moved — mesh is as much an org change as a tech change.

Trade-offs. You’re trading the simplicity of one account and one catalog for operational complexity across many accounts: more IAM, more Terraform, a tag taxonomy to govern, and RAM/Lake Formation mechanics to learn. There’s real upfront platform investment (Control Tower, AFT, the self-serve modules) before the first domain benefits. And it demands organizational maturity — domains must actually staff data ownership; if they won’t, you get the worst of both worlds (decentralized chaos with no central safety net).

Anti-patterns to avoid.

Alternatives. A single-account governed lake (Lake Formation + LF-Tags, no RAM) for organizations below the domain-complexity threshold. A lake house with Redshift data sharing when most consumers are warehouse users and producers are few. Amazon DataZone / SageMaker Unified Studio as the primary surface if you want the business-catalog and subscription experience to drive Lake Formation under the hood with less hand-rolled tooling. And for cross-cloud or open-format strategies, an Apache Iceberg + open catalog approach (S3 Tables / Glue Iceberg REST catalog) layered with the same Lake Formation governance — the mesh pattern holds; only the table format and catalog surface change.

Going deeper

Two sharing models: centralized catalog vs direct producer-to-consumer

Lake Formation cross-account sharing supports two topologies, and picking one is an early architectural decision:

The cross-account share “version” matters

The Data Catalog carries a cross-account version setting (it has incremented over time: 1 → 2 → 3 → 4). Older versions share by named resource and lean on manual RAM shares; version 3+ lets you grant directly to an AWS Organization, an OU, or an IAM principal (not just a bare account ID) and manages the RAM plumbing for you — which is what makes org-wide LF-Tag grants practical instead of a per-account chore. Raising the version changes how existing shares behave and is effectively one-way, so do it deliberately in a non-prod org first, and confirm the current maximum version in the Lake Formation docs before you pin it, since AWS keeps advancing it.

How a query actually gets its bytes — credential vending

The load-bearing idea is credential vending. When Athena, EMR, or Redshift Spectrum plans a query against a governed table, the engine calls Lake Formation’s GetTemporaryGlueTableCredentials / GetTemporaryGluePartitionCredentials APIs. Lake Formation evaluates the grant and any data filter, then returns short-lived credentials scoped to only the S3 prefixes, columns, and rows the principal may see, backed by the registration role you attached in aws_lakeformation_resource. The consumer’s own IAM role never needs s3:GetObject on the producer bucket — which is why it’s safe to strip bucket policies and IAMAllowedPrincipals. If a query returns AccessDenied despite a correct LF grant, the usual suspects are: (1) the table is still in IAMAllowedPrincipals / hybrid mode so LF isn’t actually enforcing; (2) the consumer role lacks lakeformation:GetDataAccess (and glue:GetTable/GetPartitions) in its IAM policy; or (3) the domain’s KMS key policy doesn’t grant kms:Decrypt to the vending path.

Hybrid access mode — migrating off IAMAllowedPrincipals safely

You rarely flip a live lake to full Lake Formation enforcement overnight. Hybrid access mode lets a table be governed by Lake Formation for opted-in principals while everyone else still reads it via classic IAM — so you migrate one team at a time, verify, then remove the IAM path. It’s the pragmatic bridge that the blunt “just remove IAMAllowedPrincipals” advice skips over on a brownfield lake with live consumers.

Data filters, precisely

A data filter has two independent parts: a row filter (FilterExpression, a static SQL-style predicate such as region_code='APAC', or AllRowsWildcard) and a column spec (an explicit include list, or a ColumnWildcard with ExcludedColumnNames). Row-only = row-level security; column-only = column-level security; both together = cell-level. Two caveats that trip people up: expressions are static (no “current user” function — model per-user cohorts as separate filters granted to separate roles), and you grant the filter, not the base table, to the restricted principal — grant the table by mistake and the filter never applies.

Scale, quotas, and performance

Table formats and the SageMaker direction

The mesh pattern is table-format-agnostic; only the storage and catalog surface change. Two currents worth tracking:

Practice challenges

Work these in order — they climb from “can you read the model” to “can you design and debug it.” Try each before opening the solution.

1. Match the symptom to the broken principle (beginner)

A team complains: “Every analyst in the BI account can s3:GetObject the entire Finance bucket, PII columns included.” Which of the four data-mesh principles is most directly violated, and which AWS control fixes it?

<details> <summary>Solution</summary>

Federated computational governance — access isn’t governed at all; it’s blanket bucket IAM. The fix: register the bucket as a Lake Formation location, remove IAMAllowedPrincipals, and grant via LF-Tag policies with a column filter hiding the PII columns.

Why: the governance principle means every read is an explicit, attribute-based, audited grant — never standing bucket access.

</details>

2. Create the tag taxonomy (beginner)

Write the single aws CLI command that defines a sensitivity LF-Tag with the four values used throughout this lesson.

<details> <summary>Solution</summary>

aws lakeformation create-lf-tag \
  --tag-key sensitivity \
  --tag-values public internal confidential pii

Why: LF-Tags are defined once, centrally, in the governance account; every later grant references these attribute values instead of individual table names.

</details>

3. Register storage with Lake Formation (intermediate)

Write the Terraform to register s3://acme-marketing-dataproducts as a Lake Formation location using the role arn:aws:iam::111111111111:role/lf-register.

<details> <summary>Solution</summary>

resource "aws_lakeformation_resource" "marketing_bucket" {
  arn      = "arn:aws:s3:::acme-marketing-dataproducts"
  role_arn = "arn:aws:iam::111111111111:role/lf-register"
}

Why: registration hands the S3 location to Lake Formation’s credential-vending role, so LF — not raw S3 IAM — mediates every read. The bucket ARN has no /* suffix; you register the location, not the objects.

</details>

4. The grant that scales (intermediate)

An analytics-query role in account 333333333333 should be able to SELECT every public and internal table across all domains, forever. Write the tag-policy grant in Terraform.

<details> <summary>Solution</summary>

resource "aws_lakeformation_permissions" "analytics_read" {
  principal   = "arn:aws:iam::333333333333:role/analytics-query"
  permissions = ["SELECT", "DESCRIBE"]

  lf_tag_policy {
    resource_type = "TABLE"
    expression {
      key    = "sensitivity"
      values = ["public", "internal"]
    }
  }
}

Why: one grant on an attribute covers all present and future matching tables — the difference between a mesh that scales and per-table grants that recreate the bottleneck.

</details>

5. Cell-level security (advanced)

Finance’s gl_transactions must (a) never expose employee_salary to the regional-controller role and (b) show that role only region_code = 'APAC' rows. Write the data cells filter.

<details> <summary>Solution</summary>

aws lakeformation create-data-cells-filter --table-data '{
  "TableCatalogId": "222222222222",
  "DatabaseName":   "finance_curated",
  "TableName":      "gl_transactions",
  "Name":           "apac_controller_no_salary",
  "RowFilter":      { "FilterExpression": "region_code = '\''APAC'\''" },
  "ColumnWildcard": { "ExcludedColumnNames": ["employee_salary"] }
}'

Then grant-permissions on the filter (not the table) to regional-controller.

Why: the column exclusion enforces column-level security and the row predicate enforces row-level; together they’re cell-level. Row expressions are static, so APAC is one filter of several — one per region — each granted to the matching role.

</details>

6. Debug the AccessDenied (advanced)

A consumer role has a correct LF-Tag SELECT grant and a resource link, yet SELECT * FROM sales_curated_link.orders returns AccessDenied. List the three most likely causes and how you’d confirm each.

<details> <summary>Solution</summary>

  1. Table still in IAMAllowedPrincipals / hybrid mode — LF isn’t actually enforcing, so the grant is ignored and legacy IAM (which denies) wins. Confirm: Lake Formation console → the database/table still lists IAMAllowedPrincipals under permissions.
  2. Consumer role missing lakeformation:GetDataAccess (and glue:GetTable/GetPartitions) in its IAM policy — LF authorises, but the role can’t call the vending API. Confirm: read the role’s IAM policy.
  3. KMS key policy on the producer’s CMK doesn’t grant kms:Decrypt to the vending path — LF vends creds but the decrypt fails. Confirm: inspect the CMK key policy.

Why: Lake Formation authorises what you can see, but IAM must still let the principal call the LF/Glue APIs and KMS must allow the decrypt — three independent layers that all have to line up.

</details>

Common beginner mistakes

Glossary

AWSArchitectureEnterpriseReference Architecture
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments