Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Build a Data Lake on S3

By Kokil Thapa | Last reviewed: August 2026

If you need to store structured, semi-structured, and raw logs in one place without managing a proprietary warehouse, the most practical approach is to build a data lake on S3. Unlike traditional databases that force schema-on-write, an S3-based lake decouples storage from compute, letting you ingest cheaply now and apply structure later via Spark, Athena, or Glue. In my experience working on production systems that handle everything from eCommerce transaction logs to legal-tech document metadata, getting the folder hierarchy and lifecycle rules right at the start prevents costly rewrites and massive egress bills down the road.

How do you architect a scalable S3 data lake layout?

The single biggest mistake engineers make when they first migrate from shared hosting or monolithic databases to object storage is treating S3 like a filesystem. It is not. S3 is a key-value store where the "key" happens to look like a path. When you build a data lake on S3, your partitioning strategy dictates query performance and cost more than any other factor.

Hive-style partitioning is non-negotiable

Always use Hive-compatible partitioning (key=value/). This allows query engines like Athena, Trino, and Spark to perform partition pruning automatically. If you query for WHERE date = '2026-08-22', the engine reads only the relevant prefix instead of scanning terabytes of unrelated objects.

# Recommended production layout for multi-source ingestion
s3://my-datalake-prod/
├── raw/                    # Immutable landing zone (bronze)
│   ├── ecommerce-orders/
│   │   └── year=2026/month=08/day=22/
│   │       └── orders_20260822T10.parquet
│   ├── legal-documents/
│   │   └── year=2026/month=08/day=22/
│   │       └── docs_batch_001.json.gz
│   └── application-logs/
│       └── year=2026/month=08/day=22/hour=14/
│           └── app-log-part-000.snappy.parquet
├── curated/                # Transformed business-ready data (silver/gold)
│   ├── daily-revenue/
│   └── client-intake-stats/
└── temp/                   # ETL scratch space (auto-delete after 7 days)
Three-Tier Data Lake ArchitectureRAW (Bronze)Immutable Landing Zone• JSON / CSV / Logs• Partitioned by Date• Retain 90 Days Hot• Archive to GlacierTRANSFORMETL / ELT Processing• Schema Validation• Deduplication• Parquet Conversion• Business LogicCURATED (Gold)Business-Ready Tables• Columnar Parquet• Optimized Partitions• Glue Catalog Registered• BI / ML Ready
Three-tier architecture for building a data lake on S3: raw ingestion, transformation, and curated consumption layers

Avoid the small files problem

S3 charges per request and has throughput limits per prefix. Writing thousands of 10KB files per minute will destroy your budget and throttle ingestion. On a real client project involving high-volume application logs, we consolidated writes using Kinesis Firehose buffering (set to 128MB or 900 seconds) before flushing to S3. Target file sizes between 128MB and 1GB for Parquet. If your source produces tiny files, use a compaction job (AWS Glue or Spark) to merge them hourly into the curated layer.

What storage classes and lifecycle rules minimize costs?

When you build a data lake on S3, storage cost optimization is an architectural concern, not an afterthought. S3 Intelligent-Tiering should be your default for the raw zone unless you have predictable access patterns. For most production lakes I've configured, the following lifecycle policy balances accessibility with cost.

Storage ClassBest ForRetrieval TimeCost (Approx. USD/GB/Mo)
S3 StandardHot data, active ETL, last 30 daysMilliseconds$0.023
S3 Intelligent-TieringUnknown/changing access patternsMilliseconds (hot) / Hours (cold)$0.023 + monitoring fees
S3 Glacier Instant RetrievalQuarterly compliance reports, audit logsMilliseconds$0.004
S3 Glacier Deep ArchiveLegal retention (7+ years), disaster recovery12–48 hours$0.00099

Implement lifecycle transitions via Terraform or CloudFormation

Never set lifecycle rules manually in the console for production infrastructure. Infrastructure-as-code ensures your cost controls survive accidental deletions and are reviewable in pull requests.

# Example lifecycle rule for raw zone (Terraform HCL)
resource "aws_s3_bucket_lifecycle_configuration" "datalake_raw" {
  bucket = aws_s3_bucket.datalake_raw.id

  rule {
    id     = "tier-raw-to-glacier"
    status = "Enabled"

    filter {
      prefix = "raw/"
    }

    transition {
      days          = 90
      storage_class = "GLACIER_IR"
    }

    transition {
      days          = 365
      storage_class = "DEEP_ARCHIVE"
    }

    expiration {
      days = 2555  # 7 years for legal compliance
    }
  }

  rule {
    id     = "cleanup-temp"
    status = "Enabled"

    filter {
      prefix = "temp/"
    }

    expiration {
      days = 7
    }
  }
}

For Nepal-based businesses dealing with NPR-denominated transactions and local regulatory requirements, remember that Deep Archive retrieval can take up to 48 hours. If your legal-tech portal needs same-day access to historical case documents for court deadlines, keep those specific prefixes in Glacier Instant Retrieval instead. The price difference (~Rs 0.50 vs ~Rs 0.12 per GB/month) is negligible compared to missing a filing deadline.

S3 Lifecycle Cost Optimization TimelineDay 0S3 Standard$0.023/GBDay 90Glacier IR$0.004/GBDay 365Deep Archive$0.00099/GBDay 2555ExpirationAuto-Delete~96% Storage Cost Reduction Over 1 Year(Based on 1TB Raw Dataset)
Lifecycle policy timeline: automatic tiering reduces storage costs by 96% over one year when you build a data lake on S3

How do you secure sensitive data in an S3 data lake?

Security cannot be bolted on after ingestion. When I work on legal-tech portals handling marriage certificates, divorce filings, or notarized documents, every object must be encrypted at rest and access-controlled at the prefix level. Here is the baseline security posture for any production data lake.

  • Server-Side Encryption (SSE-S3): Enable AES-256 encryption by default via bucket policy. As of 2026, S3 encrypts all new objects automatically, but explicit bucket policies prevent misconfiguration during cross-account replication.
  • Block Public Access: Apply s3:BlockPublicAcls and s3:BlockPublicPolicy at the account level via S3 Block Public Access settings. No data lake bucket should ever have public read ACLs.
  • Prefix-Level IAM Policies: Grant ETL jobs access only to their specific source/target prefixes, never *. Use resource tags for dynamic permission boundaries.
  • VPC Endpoints: Route all S3 traffic through gateway endpoints to avoid NAT gateway charges and internet exposure. This alone saved one client ~Rs 15,000/month in egress fees.
# Bucket policy enforcing encryption and VPC-only access
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnencryptedObjectUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-datalake-prod/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "AES256"
        }
      }
    },
    {
      "Sid": "DenyNonVPCAccess",
      "Effect": "Deny",
      "Principal": "*",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::my-datalake-prod",
        "arn:aws:s3:::my-datalake-prod/*"
      ],
      "Condition": {
        "StringNotEquals": {
          "aws:sourceVpce": "vpce-0abc123def456789"
        }
      }
    }
  ]
}

How do you integrate AWS Glue Catalog for SQL queries?

Raw files in S3 are useless to analysts without metadata. The AWS Glue Data Catalog acts as a centralized metastore, letting you query S3 data with standard SQL via Athena without loading it into a separate database. This is what makes the phrase "build a data lake on S3" mean something beyond cheap storage.

Register partitions automatically with crawlers or ETL

For production workloads, avoid scheduled crawlers. They are slow, expensive, and often infer schemas incorrectly when data drifts. Instead, update the Glue Catalog directly from your ETL jobs using the boto3 Glue client or Spark's built-in catalog integration.

# Python: Register new partition after ETL write
import boto3
from datetime import datetime

glue = boto3.client('glue', region_name='ap-south-1')

today = datetime.utcnow().strftime('%Y/%m/%d')
partition_values = today.split('/')

glue.batch_create_partition(
    DatabaseName='datalake_curated',
    TableName='daily_revenue',
    PartitionInputList=[{
        'Values': partition_values,
        'StorageDescriptor': {
            'Location': f's3://my-datalake-prod/curated/daily-revenue/year={partition_values[0]}/month={partition_values[1]}/day={partition_values[2]}/',
            'InputFormat': 'org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat',
            'OutputFormat': 'org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat',
            'SerdeInfo': {'SerializationLibrary': 'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe'},
            'Columns': [
                {'Name': 'order_id', 'Type': 'string'},
                {'Name': 'revenue_npr', 'Type': 'decimal(12,2)'},
                {'Name': 'currency', 'Type': 'string'},
                {'Name': 'created_at', 'Type': 'timestamp'}
            ]
        }
    }]
)
Glue Catalog Integration FlowS3 Data LakeParquet FilesPartitioned PrefixesEncrypted at RestGlue CatalogMetadata StoreSchema RegistryPartition IndexQuery EnginesAmazon AthenaSpark / EMRBI DashboardsRegisterMetadataSQL QueryScan Plan
Glue Catalog bridges S3 storage and SQL query engines, enabling serverless analytics when you build a data lake on S3

What common mistakes derail S3 data lake projects?

Having debugged several failed data lake initiatives, these anti-patterns appear repeatedly. Avoiding them saves weeks of rework and significant operational overhead.

  1. No data quality gates: Dumping malformed JSON into the raw zone without validation corrupts downstream tables. Implement schema enforcement at ingestion (Kinesis Firehose with Lambda transformation, or Glue ETL with schema registry). Reject bad records to a quarantine prefix for investigation.
  2. Ignoring GDPR/local privacy laws: Even in Nepal, if you process EU citizen data or plan international expansion, PII must be identifiable and deletable. Use column-level encryption or tokenization for sensitive fields. Never store raw PII in the curated layer without explicit business justification.
  3. Over-partitioning: Creating partitions by hour for low-volume sources generates millions of tiny objects and slows catalog operations. Only partition by dimensions that reduce scan size by at least 10x. For sparse data, consider bucketing within partitions instead.
  4. Missing monitoring: Without CloudWatch metrics on bucket size, request counts, and 4xx/5xx errors, you won't notice runaway costs until the bill arrives. Set up S3 Storage Lens and billing alarms at 50%, 80%, and 100% of budget threshold.

For teams evaluating whether a full data lake is necessary versus a simpler solution, reviewing database-driven development patterns can clarify when PostgreSQL or MySQL suffices versus when you truly need decoupled storage and compute. Not every analytics requirement justifies S3 complexity.

Build a Data Lake on S3: Next Steps

When you build a data lake on S3 correctly, you gain a foundation that scales from gigabytes to petabytes without architectural changes. Start with the three-tier layout, enforce encryption and VPC access from day one, automate lifecycle transitions, and integrate Glue Catalog early so your data is queryable immediately. Monitor costs aggressively and validate schemas before they pollute downstream analytics. If you're planning a data lake implementation and need hands-on architecture guidance tailored to your workload, reach out to discuss your specific requirements.

Frequently Asked Questions

A data lake on S3 is a centralised repository that stores raw data in its native format—structured, semi-structured, or unstructured—on Amazon S3. For Nepal-based businesses, it eliminates the need for expensive on-premises storage and scales seamlessly as data grows. Use cases include analytics for eCommerce sales (WooCommerce/Magento exports), legal document archives (PDFs, scans), or IoT sensor data from manufacturing. S3’s pay-as-you-go model (Rs 2.50/GB/month, ~USD 0.023) is far cheaper than local NAS setups, and you avoid hardware failures or power outages common in Kathmandu.

For a small business storing 1TB of data with 100,000 monthly requests, expect Rs 2,500–3,500/month (~USD 19–26). This includes S3 Standard storage (Rs 2.50/GB), PUT/GET requests (Rs 0.05 per 1,000), and minimal data transfer (Rs 0.90/GB outbound). Add Rs 1,500–2,500 (~USD 11–19) for AWS Glue crawlers (if using Athena) or Rs 5,000 (~USD 37) for a t3.small EC2 instance if running custom ETL scripts. Use S3 Intelligent-Tiering to auto-move older data to cheaper tiers (Rs 1.20/GB for infrequent access).

You need three AWS resources: 1) An S3 bucket with versioning enabled (to recover from accidental deletes), 2) An IAM role with S3 read/write permissions (attach `AmazonS3FullAccess` policy temporarily, then scope down), and 3) A VPC with a NAT gateway (Rs 3,000/month, ~USD 22) if your ETL scripts run in private subnets. For analytics, add AWS Glue (to crawl data) and Athena (to query via SQL). Skip Glue if using custom scripts (Python/boto3) or tools like Apache Spark on EMR.

Use a consistent prefix pattern: `s3://your-bucket/{domain}/{source}/{year}/{month}/{day}/`. For example, `s3://nepal-retail/raw/woocommerce/2026/05/15/orders.json`. Partition by date to enable Athena partition pruning (faster queries, lower costs). For nested data (e.g., JSON with arrays), flatten into Parquet/ORC using AWS Glue or a Lambda function. Avoid spaces or special characters in keys—stick to lowercase, hyphens, and underscores. Use S3 Lifecycle policies to auto-archive old data to Glacier (Rs 0.40/GB) after 90 days.

Use Parquet for analytics workloads (Athena, Redshift Spectrum). It’s columnar, compresses well (3–5x smaller than JSON), and supports predicate pushdown (faster queries). ORC is similar but less common in AWS tools. JSON is fine for raw ingestion but expensive to query (Athena charges by data scanned). Convert JSON to Parquet using AWS Glue ETL (Python shell) or a Lambda function with `pyarrow`. For example: `df = pd.read_json('s3://bucket/raw.json'); df.to_parquet('s3://bucket/processed.parquet')`.

Enable S3 Object Lock (WORM mode) to prevent tampering with legal/financial data. Use bucket policies to restrict access by IAM roles (e.g., deny all except `arn:aws:iam::123456789012:role/analytics-team`). Encrypt data at rest with SSE-S3 (free) or SSE-KMS (Rs 1 per 10,000 requests, ~USD 0.01) for stricter control. Enable S3 Access Logs to track who accessed what, and use AWS Macie (Rs 1.50/GB scanned, ~USD 0.011) to detect sensitive data (e.g., PAN numbers). For cross-border data, use S3’s "Nepal" bucket location (Mumbai region) to comply with local regulations.

Yes, use Amazon Athena. It’s serverless SQL over S3 (pay per query: Rs 5 per TB scanned, ~USD 0.04). First, define a table in AWS Glue Data Catalog (or let Glue crawl your S3 bucket). Example query: `SELECT FROM "db"."sales" WHERE dt = '2026-05-15'`. For frequent queries, use Athena Workgroups to set cost controls (e.g., Rs 1,000/month cap). For large datasets, partition tables by date to reduce scanned data. Avoid `SELECT `—filter early to save costs.

For Laravel, use the `aws/aws-sdk-php` package (v3.280+). Example: `Storage::disk('s3')->put('raw/orders/2026-05-15.json', $orderData)`. For WordPress, use WP Offload Media Lite (free) to auto-upload media to S3, or a custom plugin with `wp_remote_post` to push JSON exports. For real-time ingestion, use Amazon Kinesis Firehose (Rs 0.025/GB ingested, ~USD 0.0002) to batch and compress data before S3. For large exports, use AWS DataSync (Rs 0.04/GB, ~USD 0.0003) to sync local files to S3.

Use AWS Glue Schema Registry (free) to track schema versions. For Parquet/ORC, add new columns without breaking old queries (backward-compatible). For JSON, use a `metadata.json` file in each prefix to document schema. Example: `{"version": "2.1", "fields": ["id", "name", "new_field"]}`. For breaking changes, write to a new prefix (e.g., `raw_v2/`) and update Glue crawlers. Avoid renaming columns—add new ones instead. For validation, use Lambda with `jsonschema` (Python) to reject malformed data before it hits S3.

Set up AWS Cost Explorer with a filter for `Service = S3` and `Usage Type = Storage`. Create a budget alert (free) for Rs 5,000/month (~USD 37) to avoid surprises. Use S3 Storage Lens (free tier) to track storage growth by prefix. For query costs, enable Athena query logging to CloudWatch (Rs 0.50/GB, ~USD 0.004) and set up a Lambda to alert if a query scans >10GB. Tag all S3 objects with `Project=Retail` or `Environment=Prod` to allocate costs. Use S3 Intelligent-Tiering to auto-move data to cheaper tiers (Rs 1.20/GB for infrequent access).

Yes. For Power BI, use the Amazon Athena connector (free) or the S3 connector (preview). For Tableau, use the Athena or S3 connector (Tableau Desktop 2023.1+). Both tools push queries to Athena, so costs are the same as querying directly (Rs 5/TB scanned). For large datasets, pre-aggregate data in S3 using AWS Glue or a Lambda function. Example: `SELECT date_trunc('day', order_date) AS day, SUM(amount) FROM sales GROUP BY 1`. Store results in a new prefix (e.g., `aggregated/daily_sales/`) to reduce query costs.

1) Not partitioning data by date—queries scan entire datasets, costing more. 2) Using JSON instead of Parquet—Athena charges by data scanned, not rows. 3) Ignoring S3 Lifecycle policies—old data piles up, inflating costs. 4) Overlooking IAM permissions—granting `s3:` instead of scoped access. 5) Not enabling versioning—accidental deletes become permanent. 6) Storing credentials in code—use IAM roles or AWS Secrets Manager (Rs 0.40/secret/month, ~USD 0.003). 7) Skipping data validation—malformed JSON breaks Glue crawlers. 8) Not monitoring costs—small queries add up (e.g., 100 queries scanning 1GB each = Rs 500/month, ~USD 3.70).

Enable S3 Versioning and Cross-Region Replication (CRR) to a second bucket in `ap-south-1` (Mumbai) or `ap-southeast-1` (Singapore). For compliance, use S3 Object Lock in Governance mode (prevents deletions for a set period). For disaster recovery, use AWS Backup (Rs 0.05/GB/month, ~USD 0.0004) to create immutable backups. Test restores monthly—don’t assume versioning works without verification. For critical data, export to Glacier Deep Archive (Rs 0.10/GB/month, ~USD 0.0007) with a 7-year retention policy.

Yes. Use Amazon SageMaker to train models on data in S3. Example: Store transaction data in `s3://your-bucket/raw/transactions/` (Parquet format), then use SageMaker’s built-in algorithms (e.g., Random Cut Forest for anomaly detection). Costs: Rs 10–50/hour (~USD 0.07–0.37) for training jobs, plus S3 storage. For real-time inference, deploy the model as an endpoint (Rs 5–20/hour, ~USD 0.04–0.15) and stream data via Kinesis. For simpler use cases, use AWS Glue ML Transforms to detect anomalies without writing code.

A data lake (S3) stores raw data in open formats (Parquet, JSON) and scales infinitely. A data warehouse (Redshift) stores structured data in a proprietary format and is optimised for SQL analytics. Use a data lake for: 1) Unstructured data (logs, PDFs), 2) Schema flexibility (add columns without migrations), 3) Cost efficiency (Rs 2.50/GB vs Redshift’s Rs 5–10/GB). Use a data warehouse for: 1) Complex joins, 2) High-concurrency queries, 3) Pre-aggregated dashboards. Many businesses use both: ingest raw data into S3, then load cleaned data into Redshift for analytics.

Share this article

Quick Contact Options
Choose how you want to connect me: