<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: elliott cordo</title>
    <description>The latest articles on DEV Community by elliott cordo (@elliott_cordo).</description>
    <link>https://dev.to/elliott_cordo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F985543%2Fffe44725-22a1-4a62-a793-75e185a55178.jpg</url>
      <title>DEV Community: elliott cordo</title>
      <link>https://dev.to/elliott_cordo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elliott_cordo"/>
    <language>en</language>
    <item>
      <title>Another Data Nerd Guide to re:Invent 2025</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Tue, 14 Oct 2025 00:34:35 +0000</pubDate>
      <link>https://dev.to/aws-heroes/another-data-nerds-guide-to-reinvent-2025-4oo6</link>
      <guid>https://dev.to/aws-heroes/another-data-nerds-guide-to-reinvent-2025-4oo6</guid>
      <description>&lt;p&gt;Well, it's that time of year again.   In less than two months we'll be in amazing and weird Las Vegas, hustling between venues, trying to maximize learning, networking, and perhaps a fun Las Vegas side quest.    &lt;/p&gt;

&lt;p&gt;As exciting as this time can be, it's also incredibly daunting (there are nearly 3000 sessions 😱).  And to make matters worse you need to have your favorites ready, as in just a few hours we'll need to book sessions.  This experience is stressful, very much like buying in demand tickets for a concert or sporting event tickets.  &lt;/p&gt;

&lt;p&gt;So where to start?&lt;/p&gt;

&lt;h3&gt;
  
  
  My theme for this year - AI Convergence
&lt;/h3&gt;

&lt;p&gt;We’re witnessing the long-anticipated convergence of data and software engineering — data systems are finally being built and operated like software. At the same time, Generative AI is accelerating this shift, demanding production-grade reliability, automation, and integration into core products. Together, these forces are reshaping what it means to be a data professional.&lt;br&gt;
You can read more about my point of view &lt;a href="https://medium.com/datafutures/data-software-and-ai-convergence-part-1-a26c615bc2a8" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;So, with this theme in mind, here's what's on my list.   It's not perfect, but it's a good start for my data friends who are trying to up-skill and jump into the convergence head on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data is being built like software.
&lt;/h3&gt;

&lt;p&gt;Data Mesh and Open Data Lake format are driving micro-service like architecture, and having clean, reliable data is more important than ever.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building Production-Ready Data Systems for AI Applications (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=ANT403" rel="noopener noreferrer"&gt;ANT403&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Building an operational data lake using S3 Tables and SageMaker (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=STG413" rel="noopener noreferrer"&gt;STG413&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Build trust in your data with Amazon SageMaker Catalog (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=ANT406" rel="noopener noreferrer"&gt;ANT406&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data professionals need to be full stack, and serverless is the best way to start. 🤓&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Serverless developer experience workshop (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=CNS402" rel="noopener noreferrer"&gt;CNS402&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Building Serverless distributed data processing workloads (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=CNS404" rel="noopener noreferrer"&gt;CNS404&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Software is becoming intelligent. And AI is forcing it all to converge — faster than ever.
&lt;/h3&gt;

&lt;p&gt;Learn the frameworks and tools to build agents leverage MCP (Model Context Protocol) and RAG (Retrieval Augmented Generation). In order of recommendation -- essential to extra credit.   It's a long list, but the solution space is large and evolving. 🤖&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent in an hour: Build an agentic app with Strands Agents and MCP (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=IND334" rel="noopener noreferrer"&gt;IND334&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Vector search with Amazon OpenSearch Service (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=ANT419" rel="noopener noreferrer"&gt;ANT419&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Unlock interoperability: Build your first MCP server (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=DVT404" rel="noopener noreferrer"&gt;DVT404&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Deploying Intelligent Agent Systems with MCP (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=PEX401" rel="noopener noreferrer"&gt;PEX401&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Smart agents meet documents: Building next-gen IDP architectures (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=AIM415" rel="noopener noreferrer"&gt;AIM415&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Build agentic workflows with Small Language Models and SageMaker AI (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=AIM406" rel="noopener noreferrer"&gt;AIM406&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Efficient AI model customization for agentic workflows (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=AIM410" rel="noopener noreferrer"&gt;AIM410&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Implement hybrid search with Aurora PostgreSQL for MCP retrieval (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=DAT409" rel="noopener noreferrer"&gt;DAT409&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security and governance 🔒&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mastering agent authentication with Amazon Bedrock AgentCore Identity (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=AIM321" rel="noopener noreferrer"&gt;AIM321&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Red Team vs Blue Team: Securing AI Agents (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=DEV317" rel="noopener noreferrer"&gt;DEV317&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Build trustworthy AI applications with Amazon Bedrock Guardrails  (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=AIM303" rel="noopener noreferrer"&gt;AIM303&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;And a little fun 🎊&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create your own AI sidekick: a hands-on agent building workshop (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=DVT403" rel="noopener noreferrer"&gt;DVT403&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Game on: build a retro adventure game in 120 minutes (&lt;a href="https://registration.awsevents.com/flow/awsevents/reinvent2025/event-catalog/page/eventCatalog?search=DVT402" rel="noopener noreferrer"&gt;DVT402&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this guide is helpful, have fun and get weird 😄.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dataengineering</category>
      <category>genai</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Federated Airflow with SQS</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Sat, 06 Jul 2024 12:34:09 +0000</pubDate>
      <link>https://dev.to/aws-heroes/federated-airflow-with-sqs-36bg</link>
      <guid>https://dev.to/aws-heroes/federated-airflow-with-sqs-36bg</guid>
      <description>&lt;p&gt;Simple Queueing Services (SQS) is one of the most simple and effective services AWS has to offer.    The thing I like most about it is it's versatility, performing equality well in high volume pub-sub event processing and more general low-volume orchestration.   In this post we'll review how we can use SQS to create non-monolithic Airflow architecture, a double-click into my &lt;a href="https://dev.to/aws-heroes/the-wrath-of-unicron-when-airflow-gets-scary-27kg"&gt;previous post&lt;/a&gt; on the subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  More Airflows = More Happy
&lt;/h2&gt;

&lt;p&gt;Airflow is a great tool and at this point fairly ubiquitous in the data engineering community. However the more complex the environment the more difficult it will be to develop/deploy, and ultimately the less stable it will be.&lt;/p&gt;

&lt;p&gt;I generally recommend the following principles when architecting with Airflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small maintainable data product repos&lt;/li&gt;
&lt;li&gt;Multiple purposeful Airflow environments &lt;/li&gt;
&lt;li&gt;Airflow environments communicating through events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this all sounds good, but what about dependencies 😬??? Organizations are going to have some dependencies that cross domain/product boundaries. We'd stand to lose that Airflow dependency goodness if the products are running in separate Airflow environments.   A good example of this would be a "customer master", which might be used in several independently developed data products.  That's where "communicating through events" comes in 😃&lt;/p&gt;

&lt;h2&gt;
  
  
  SQS to the rescue
&lt;/h2&gt;

&lt;p&gt;Luckily this problem is VERY easily solved using SQS, and we've put together &lt;a href="https://github.com/datafuturesco/template-sns-demo" rel="noopener noreferrer"&gt;this little demo repository&lt;/a&gt; to help you get started.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Setup
&lt;/h3&gt;

&lt;p&gt;Assuming you already have two MWAA environments or self-hosted Airflow infrastructure you will need to &lt;a href="https://github.com/datafuturesco/template-sns-demo/blob/main/dags/create_sns_topic.py" rel="noopener noreferrer"&gt;create an SNS topic&lt;/a&gt; and &lt;a href="https://github.com/datafuturesco/template-sns-demo/blob/main/dags/create_sqs_queue.py" rel="noopener noreferrer"&gt;create and subscribe an SQS subscription&lt;/a&gt;.   We decided to be cute and package these as DAG's, but you can lift the code or create from the console. &lt;/p&gt;

&lt;p&gt;You will then need to attach a policy to the role used by your Airflow environment.  Note that this policy is fairly permissive as they will enable the steps above to be run through Airflow.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowSNSActions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:CreateTopic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:DeleteTopic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:ListTopics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:ListSubscriptionsByTopic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:GetSubscriptionAttributes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:Subscribe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:SetSubscriptionAttributes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:ConfirmSubscription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:Publish"&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sns:&lt;em&gt;:{AccountID}:&lt;/em&gt;"&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowSQSActions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sqs:CreateQueue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sqs:DeleteQueue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sqs:GetQueueUrl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sqs:GetQueueAttributes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sqs:SetQueueAttributes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sqs:ListQueues"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sqs:ReceiveMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sqs:DeleteMessage"&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sqs:&lt;em&gt;:{AccountID}:&lt;/em&gt;"&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;&lt;br&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Step 2: Create SNS Publish DAG&lt;br&gt;
&lt;/h3&gt;

&lt;p&gt;In the following DAG we create a simulated upstream dependency(consider this your &lt;em&gt;customer master&lt;/em&gt; build step).    We use the &lt;code&gt;SnsPublishOperator&lt;/code&gt; to notify downstream dependencies after our dummy step is complete.  &lt;/p&gt;

&lt;p&gt;⚠️  Note that if you did not build your SNS/SQS resources using the DAGS, you will need to manually set your Airflow variables with the appropriate ARN's.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;p&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.operators.bash&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BashOperator&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.amazon.aws.operators.sns&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SnsPublishOperator&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.operators.python&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PythonOperator&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Variable&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.utils.dates&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;days_ago&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="n"&gt;default_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;br&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airflow&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;days_ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="c1"&gt;# Define DAG to display the cross-dag dependency using SNS topic publish&lt;br&gt;
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;br&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sns_publish_dummy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;A simple DAG to publish a message to an SNS topic&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;schedule_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;br&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br&gt;
    &lt;span class="c1"&gt;# Dummy task to show upward dag dependency success&lt;br&gt;
&lt;/span&gt;    &lt;span class="n"&gt;dummy_sleep_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BashOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sleep_task&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;bash_command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sleep 10&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;br&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;span class="c1"&amp;gt;# SNS Publish operator to publish message to SNS topic after the upward tasks are successful
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;    &lt;span class="n"&gt;publish_to_sns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SnsPublishOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;publish_to_sns&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;target_arn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Variable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sns_test_arn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# SNS topic arn to which you want to publish the message&lt;br&gt;
&lt;/span&gt;        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;This is a test message from Airflow&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Test SNS Message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;br&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;span class="n"&amp;gt;dummy_sleep_task&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;&amp;amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;publish_to_sns&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;&lt;strong&gt;name&lt;/strong&gt;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&lt;strong&gt;main&lt;/strong&gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;/p&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Step 3: Create SQS Subscribe DAG&lt;br&gt;
&lt;/h3&gt;

&lt;p&gt;This DAG will simulate the downstream dependency, perhaps a &lt;em&gt;customer profile&lt;/em&gt; job.  Leveraging the &lt;code&gt;SqsSensor&lt;/code&gt;, it simply waits for the upstream job to complete, and then runs it's own dummy step.  Note that the &lt;code&gt;mode='reschedule'&lt;/code&gt; is required to enable this polling/waiting functionality.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;p&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.amazon.aws.sensors.sqs&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SqsSensor&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.operators.python&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PythonOperator&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Variable&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.utils.dates&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;days_ago&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="n"&gt;default_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;br&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airflow&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;days_ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;print_sqs_message&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;br&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, SQS read and delete successful!! &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="c1"&gt;# Define DAG to show cross-dag dependency using SQS sensor operator&lt;br&gt;&lt;br&gt;
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;br&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sqs_sensor_example&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;A simple DAG to sense and print messages from SQS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;schedule_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;br&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;span class="c1"&amp;gt;# SQS sensor operator waiting to receive message in the provided SQS queue from SNS topic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;    &lt;span class="n"&gt;sense_sqs_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SqsSensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sense_sqs_queue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;sqs_queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Variable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqs_queue_test_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;# Airflow variable name for the SQS queue url &lt;br&gt;
&lt;/span&gt;        &lt;span class="n"&gt;aws_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aws_default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;max_messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;wait_time_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;visibility_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
        &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reschedule&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# the task waits for any message to be received in the specified queue&lt;br&gt;
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;span class="n"&amp;gt;print_message&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="nc"&amp;gt;PythonOperator&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;
    &amp;lt;span class="n"&amp;gt;task_id&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class="sh"&amp;gt;'&amp;lt;/span&amp;gt;&amp;lt;span class="s"&amp;gt;print_message&amp;lt;/span&amp;gt;&amp;lt;span class="sh"&amp;gt;'&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;
    &amp;lt;span class="n"&amp;gt;python_callable&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;print_sqs_message&amp;lt;/span&amp;gt;
&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;

&amp;lt;span class="n"&amp;gt;sense_sqs_queue&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;&amp;amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;print_message&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;&lt;strong&gt;name&lt;/strong&gt;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&lt;strong&gt;main&lt;/strong&gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;/p&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Testing, Testing&lt;br&gt;
&lt;/h3&gt;

&lt;p&gt;Once your environment is setup, simply start your SQS subscriber DAG.  It will patiently wait polling SQS for a completion state.   &lt;/p&gt;

&lt;p&gt;When you are ready start your SNS publisher DAG.   Once complete your subscriber will start it's dummy step and complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing it all together..
&lt;/h2&gt;

&lt;p&gt;Big picture, leveraging SQS you can enable a pragmatic, data-mesh inspired infrastructure like has been illustrated below.   No Airflow-based single point of failure, observed domain/product boundaries, and team autonomy.   &lt;/p&gt;

&lt;p&gt;As a bonus you have also enabled evolutionary architecture.   If some team wants to transition from Airflow to Step Functions, or Prefect they are empowered to do so, so long as they continue interacting through SNS/SQS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fty96punl7zw27to5lmzh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fty96punl7zw27to5lmzh.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd like to give special thanks to &lt;a class="mentioned-user" href="https://dev.to/deekshagunde"&gt;@deekshagunde&lt;/a&gt; for contributing to this article and preparing the demo repo.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>MWAA Plugins and Dependency Survival Guide</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Fri, 05 Apr 2024 12:50:21 +0000</pubDate>
      <link>https://dev.to/aws-heroes/mwaa-plugins-and-dependencies-survival-guide-27ao</link>
      <guid>https://dev.to/aws-heroes/mwaa-plugins-and-dependencies-survival-guide-27ao</guid>
      <description>&lt;p&gt;Very often it makes sense to use a managed service instead of undifferentiated heavy lifting of &lt;em&gt;properly&lt;/em&gt; building and maintaining infrastructure.   For me, managing Apache Airflow definitely falls into this category and I often use AWS MWAA (Managed Workflows for Apache Airflow).&lt;/p&gt;

&lt;p&gt;As many of you have worked with Airflow already know, customizations, especially modifications to the Python environment can be tricky, and in some cases dangerous.  This is mainly due to the fact that Airflow itself is a complex Python application with it's own environmental considerations and dependencies.  &lt;/p&gt;

&lt;p&gt;This is why I continue to campaign that folks keep their Airflow environment small and purposeful, and reduce customizations by using tools like the &lt;a href="https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/stable/operators.html" rel="noopener noreferrer"&gt;pod operator&lt;/a&gt;.  I detail much of this in my article &lt;a href="https://dev.to/aws-heroes/the-wrath-of-unicron-when-airflow-gets-scary-27kg"&gt;The Wrath of Unicron&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However it's very difficult to stay completely vanilla 🍦, so here are a few tips when customizing the MWAA environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tip 0: Use MWAA Local Runner
&lt;/h3&gt;

&lt;p&gt;I won't go into great detail here, because the docs are quite good.  But you should absolutely be developing, and testing all changes leveraging &lt;a href="https://github.com/aws/aws-mwaa-local-runner" rel="noopener noreferrer"&gt;MWAA Local Runner&lt;/a&gt;.   It's very close to the real thing and you will avoid waiting for changes to propagate in the actual MWAA environment (my one complaint is 20-40 minutes for an environment update is kinda crazy).&lt;/p&gt;

&lt;h3&gt;
  
  
  Tip 1: LOGGING!!
&lt;/h3&gt;

&lt;p&gt;Before you start any customization, turn your logging up to 11.   &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxmd3zb8o9rmz1mnaq90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxmd3zb8o9rmz1mnaq90.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will need all the detailed log entries, especially for the Scheduler.    If your MWAA environment is not recognizing your changes, or getting stuck in the updating state (crash loop), check the &lt;em&gt;requirements&lt;/em&gt; log entries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficjo0rl45gza9e1fu0eg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficjo0rl45gza9e1fu0eg.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tip 2: Constraints File
&lt;/h3&gt;

&lt;p&gt;For the past several versions of Airflow, a public constraints file has been published and maintained.   This constraint file protects Airflows dependencies and makes sure that customizations do not break things.  &lt;/p&gt;

&lt;p&gt;⚠️ With MWAA, messing up dependencies can cause the before-mentioned crash loop, which can often last for hours 😭.&lt;/p&gt;

&lt;p&gt;A constraint statement pointing to this file must be referenced in the top of your requirements.txt and will look something like this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--constraint "https://raw.githubusercontent.com/apache/airflow/constraints-{Airflow-version}/constraints-{Python-version}.txt"&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;em&gt;DO NOT OMIT THE CONSTRAINT STATEMENT!!!&lt;/em&gt;
&lt;/h1&gt;

&lt;p&gt;..but if you can't make the default file work, see Tip 3 below 😃&lt;/p&gt;

&lt;h3&gt;
  
  
  Tip 3: Unresolvable Conflicts
&lt;/h3&gt;

&lt;p&gt;Unfortunately not all Python packages are well maintained or have tight locking to upstream dependency versions.   Over time, you can run into unresolvable conflicts between your packages and plugins, and the constraints file.    &lt;/p&gt;

&lt;p&gt;The first recommendation - upgrade to the latest version of Airflow.  It's very likely the problems you are experiencing are resolved in the latest version.  If this is not an option, I suggest certifying and hosting your own version of the constraint file.  This will involve tweaking the package dependencies and making sure they are compatible with Airflow.  &lt;/p&gt;

&lt;p&gt;This may not be a trivial process, but try your best not to comment lines, and absolutely do not remove the constraints statement all together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tip 4: Troubleshooting Plugins
&lt;/h3&gt;

&lt;p&gt;Maybe you did think you did everything right (per the &lt;a href="https://docs.aws.amazon.com/mwaa/latest/userguide/configuring-dag-import-plugins.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt;), your MWAA environment is booting, but your Plugins are not installing.&lt;/p&gt;

&lt;p&gt;First, check your &lt;em&gt;requirements&lt;/em&gt; log entries and see if anything has failed during the install.   Note that in most cases the requirements install will do a complete rollback of package installs, not just the offenders.&lt;/p&gt;

&lt;p&gt;If you don't see your package being installed make sure you referenced your package correctly in your requirements file.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/usr/local/airflow/plugins/data_common_utils-0.2.8-py3-none-any.whl&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If all this looks correct, try creating a simple "plugin finder" DAG, and make sure your plugin has been copied to the hosted environment.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;p&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.utils.dates&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;days_ago&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;br&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.operators.bash&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BashOperator&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;plugin-finder&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;days_ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="n"&gt;ls_airflow_plugins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BashOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ls_airflow_plugins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;bash_command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ls -laR /usr/local/airflow/plugins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;priority_weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;br&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;/p&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Good Luck!&lt;br&gt;
&lt;/h3&gt;

&lt;p&gt;I hope you all find this helpful.  Please comment with other helpful tips!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.datafutures.io/" rel="noopener noreferrer"&gt;Data Futures&lt;/a&gt;&lt;/p&gt;

</description>
      <category>airflow</category>
      <category>bigdata</category>
      <category>dataengineering</category>
      <category>aws</category>
    </item>
    <item>
      <title>Test Driving Redshift AI-Driven Scaling</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Thu, 21 Dec 2023 15:13:32 +0000</pubDate>
      <link>https://dev.to/aws-heroes/test-driving-redshift-ai-driven-scaling-4k1m</link>
      <guid>https://dev.to/aws-heroes/test-driving-redshift-ai-driven-scaling-4k1m</guid>
      <description>&lt;p&gt;Of all the amazing announcements from 2023 Reinvent I was probably most excited about the new &lt;a href="https://aws.amazon.com/blogs/big-data/amazon-redshift-announcements-at-aws-reinvent-2023-to-enable-analytics-on-all-your-data/#:~:text=Amazon%20Redshift%20Serverless%20is%20more%20intelligent%20than%20ever%20with%20new%20AI%2Ddriven%20scaling%20and%20optimizations" rel="noopener noreferrer"&gt;scaling capabilities&lt;/a&gt; announced for AWS Redshift.  As a long time Redshift user, perhaps even the debatable customer #1, I'm always eager to learn of the new and exciting ways we can use this service.&lt;/p&gt;

&lt;p&gt;Serverless has long been a direction for AWS service offerings, and this was brought to Redshift last year with it's first serverless release.  Redshift Serverless V1 was definitely an amazing step forward for the platform.  I've implemented at many organizations to supplement existing RA3 infrastructure, or alone as the primary warehouse engine.   For workloads that are "spikey", or overall have low duty cycle we've been able to drive amazing price performance.&lt;/p&gt;

&lt;p&gt;In terms of price-performance one of the most important parameters for V1 Redshift serverless is the &lt;a href="https://docs.aws.amazon.com/redshift/latest/mgmt/serverless-capacity.html#:~:text=to%20handle%20workloads.-,Base%20capacity,-This%20setting%20specifies" rel="noopener noreferrer"&gt;base RPU&lt;/a&gt; (Redshift Processing Unit) allocation.  This parameter controls initial "warm" Redshift capacity that is ready for work.   As you can guess you want this number to be as low as possible to maintain a low baseline cost profile, which is in tension with reducing latency as it autoscales.  &lt;/p&gt;

&lt;p&gt;💸 &lt;strong&gt;IMPORTANT TIP&lt;/strong&gt; 💸 The default base RPU is 128.  When experimenting be sure to turn base RPU way down or you might have an unexpected surprise in your AWS bill.&lt;/p&gt;

&lt;p&gt;The main thing to consider here is that the &lt;em&gt;autoscaling&lt;/em&gt; is primarily for query concurrency.   When it comes to large workloads like ELT jobs, the throughput is limited to the base RPU setting.  So if you throw a large workload at an undersized serverless cluster you will not have the performance you expect.  If you maintain too high a base RPU, you will wastefully reserving capacity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdna0p8dzv4unjq2g4p0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdna0p8dzv4unjq2g4p0s.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The great thing is that since Redshift infrastructure can be manipulated by API, we can do all sorts of creative things like temporarily increasing the RPU setting before our jobs, or even spinning up an ephemeral cluster.  But luckily the new scaling options make things much easier now.&lt;/p&gt;

&lt;h1&gt;
  
  
  AI Scaling
&lt;/h1&gt;

&lt;p&gt;With the latest release Redshift Serverless now uses ML to estimate the required cluster size in order to process a submitted workload.   Instead of a base RPU setting you instead control a price performance ratio.&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv40xusijgvym58xtjnfq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv40xusijgvym58xtjnfq.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I decided to test out this service using a larger dataset of clickstream data, and troublesome ELT step that I recently refactored.  The incremental data is about 25 million rows, and the ELT step is essentially session-izing and creating user/date level aggregates.    We'd found that a 128 RPU V1 cluster was providing adequate price performance, with and average run time of just over 4 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;So let's test drive a preview cluster!&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I created a new preview cluster, accepting the default of a 50% price performance ratio and ran an incremental workload.  The cluster starting from zero hit a max RPU of 128 and completed the workload in 3.5 min and then settled back to zero.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvpdjlrzabpj5g26evi1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvpdjlrzabpj5g26evi1.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;This initial test is really promising, and In the coming month I plan to do additional testing of larger workloads, and additionally layering on some smaller concurrent queries.&lt;/p&gt;

&lt;p&gt;Overall this is a great step forward for Redshift. It will enable some really interesting topologies that mix serverless and provisioned RA3 clusters to optimize for just about any workloads, especially when leveraging the new &lt;a href="https://aws.amazon.com/about-aws/whats-new/2023/11/amazon-redshift-multi-data-warehouse-writes-data-sharing-preview/" rel="noopener noreferrer"&gt;multi-cluster writes (preview)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.datafutures.co/" rel="noopener noreferrer"&gt;Data Futures&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@kovax?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Ivan Kovac&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>bigdata</category>
      <category>dataengineering</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Avoiding the DBT Monolith</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Sun, 15 Oct 2023 15:27:19 +0000</pubDate>
      <link>https://dev.to/elliott_cordo/avoiding-the-dbt-monolith-7ep</link>
      <guid>https://dev.to/elliott_cordo/avoiding-the-dbt-monolith-7ep</guid>
      <description>&lt;p&gt;As a data engineer, unless you've been living under a rock, you've probably been working with DBT, or aspire to do so.   DBT is an great step in the right direction for data engineering, removing boilerplate tasks, and establishing observable contracts between models.   &lt;/p&gt;

&lt;p&gt;However, DBT projects can easily get out of hand.  I often see entire data platforms defined in a single DBT project (monolithic repositories).   In some organizations this doesn't cause much harm, but in many it becomes a nightmare to maintain.  This is especially true when data ecosystem is large, and when data development is more federated with data products being maintained by separate teams. Also see my &lt;a href="https://dev.to/aws-heroes/the-wrath-of-unicron-when-airflow-gets-scary-27kg"&gt;article&lt;/a&gt; on similar pitfalls with DAG's. &lt;/p&gt;

&lt;p&gt;DBT has been thinking about this, and there are some major &lt;a href="https://www.getdbt.com/blog/dbt-labs-announces-major-enhancements-to-dbt-cloud-to-enable-collaboration-at-scale"&gt;features in preview&lt;/a&gt; to help support multi-repo, federated project development.  However these features will only be available for Enterprise customers. &lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-repo Strategy
&lt;/h2&gt;

&lt;p&gt;The good news is that if you are running DBT Core, or want to leverage existing features there are options. Since DBT is built on Python, and is easily extensible with packages.  Packages are useful for utility functions, data quality, and just general code reusability.   But they can also be used to import and reference other DBT project models.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to
&lt;/h2&gt;

&lt;p&gt;For demonstration purposes I've created two repos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/elliottcordo/dbt_poc_a"&gt;Project A&lt;/a&gt; - parent repo, vanilla DBT "hello world".&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/elliottcordo/dbt_poc_b"&gt;Project B&lt;/a&gt; - child repo that inherits from Project A.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For both projects you will need to setup a virtual environment and install the appropriate dbt package &lt;code&gt;dbt-snowflake&lt;/code&gt;, &lt;code&gt;dbt-redshift&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;In order for Project B to inherit you need to simply add the parent project to &lt;code&gt;packages.yaml&lt;/code&gt;, which will be imported when you run &lt;code&gt;dbt deps&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;git&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/elliottcordo/dbt_poc_a.git"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now add the model to your &lt;code&gt;dbt_project.yaml&lt;/code&gt;.  You can also override config parameters.  This is especially helpful if your parent models exist in a different schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dbt_poc_a&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# +schema: schemaname&lt;/span&gt;
    &lt;span class="c1"&gt;# Config indicated by + and applies to all files under models/example/&lt;/span&gt;
  &lt;span class="na"&gt;dbt_poc_b&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# +schema: schemaname&lt;/span&gt;
    &lt;span class="c1"&gt;# Config indicated by + and applies to all files under models/example/&lt;/span&gt;
    &lt;span class="na"&gt;dbt_poc_b&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;+materialized&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;view&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the model specification you can now reference the models from Project A.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;materialized&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'table'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;source_data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;

    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;distinct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; 
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dbt_poc_a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'my_first_dbt_model'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;source_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When running models in the child project you will most likely want to suppress building the upstream models (with the assumption they are maintained and built by a different team).  You can use select and filter expressions in your &lt;code&gt;dbt run&lt;/code&gt; command too accomplish this: &lt;code&gt;dbt run --select dbt_poc_b&lt;/code&gt;&lt;br&gt;
⚠️ this is something you really need to be careful with!&lt;/p&gt;

&lt;p&gt;You can now run &lt;code&gt;dbt docs generate&lt;/code&gt; and &lt;code&gt;dbt docs serve&lt;/code&gt; to view dependencies and cross model metadata..&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftm8lnbofp1rson4bla9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftm8lnbofp1rson4bla9u.png" alt="Image description" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional thoughts
&lt;/h2&gt;

&lt;p&gt;Note that this approach alone will not enable &lt;em&gt;safe&lt;/em&gt; federated DBT development.  Process and culture will also come into play to avoid breaking changes to downstream models.  You should also anticipate building a good amount of internal tooling, especially in your CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;And just a reminder, if your data platform is small, your data team is small and/or all development is centralized, this approach may be pre-mature optimization. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.datafutures.co/"&gt;Data Futures&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dbt</category>
      <category>datamesh</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Running Jobs on Athena Spark</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Fri, 13 Oct 2023 19:00:17 +0000</pubDate>
      <link>https://dev.to/aws-heroes/running-jobs-on-athena-spark-2g92</link>
      <guid>https://dev.to/aws-heroes/running-jobs-on-athena-spark-2g92</guid>
      <description>&lt;p&gt;Athena Spark is hands down my favorite Spark implementations on AWS.   First off, it's a managed service and serverless, meaning you don't need to worry about clusters and you only pay for what you use.    Secondly it autoscales for a given workload and very successfully hides the complexity of Spark. Last but not least &lt;u&gt;it's instant&lt;/u&gt; - you get a useable Spark session literally within the time it takes for the Notebook editor to render.   It's magical!  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzu7f9px88lfprir3tft.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzu7f9px88lfprir3tft.gif" alt="Image description" width="200" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But what if we want to leverage this magic for production workloads? Specifically scheduled jobs, where an interactive Spark environment just doesn't fit?   Although the current version of the service is definitely optimized for interactive experience, running scheduled jobs through Athena is both possible, and magical.&lt;/p&gt;

&lt;p&gt;Here is a quick walkthrough of how you can run Athena Spark jobs through good ol' boto3.&lt;/p&gt;

&lt;p&gt;First we establish a client connection and start a session.   Note this session could be reused for several jobs, although the instantiation time is so fast there is probably not much reason to persist it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;athena&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;calculation_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;job_session&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;WorkGroup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spark_jobs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EngineConfiguration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CoordinatorDpuSize&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MaxConcurrentDpus&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calculation_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SessionId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calculation_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;State&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the session is established we can start submitting work.   Instead of referencing an existing notebook, you will instead submit your code &lt;em&gt;as a string&lt;/em&gt;.  Yes, this seems a bit weird, but after all Python is an interpreted language, stored in plain text, i.e. a string, so get over it 😃!  It would be great if you could reference an S3 URI and hopefully they will provide additional options in the future. &lt;/p&gt;

&lt;p&gt;I'd recommend maintaining this code as a separate .py file that could be mocked/tested outside this "driver" code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complicated_spark_job.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;notebook_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;execution_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_calculation_execution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;daily job&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CodeBlock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;notebook_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;calc_exec_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execution_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CalculationExecutionId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then iterate and monitor our Spark jobs progress.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;exec_status_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_calculation_execution_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CalculationExecutionId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;calc_exec_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;exec_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exec_status_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;State&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exec_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exec_state&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CANCELED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FAILED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the job completes we can terminate our session, if we have no other work to submit. Don't worry, if you don't forcibly terminate it will time out after a few minutes of idle time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to Athena Spark being fast and easy, it's also very cost effective.  DPU's are priced at $0.35 per hour, rounded to the second.   So a 1 hour 20 DPU workload (which is allot of processing power) would cost you about 7 bucks!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.datafutures.co/"&gt;Data Futures&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awsbigdata</category>
      <category>spark</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>The Wrath of Unicron - When Airflow Gets Scary</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Wed, 30 Aug 2023 12:42:41 +0000</pubDate>
      <link>https://dev.to/aws-heroes/the-wrath-of-unicron-when-airflow-gets-scary-27kg</link>
      <guid>https://dev.to/aws-heroes/the-wrath-of-unicron-when-airflow-gets-scary-27kg</guid>
      <description>&lt;p&gt;In case you weren't already a fan of the 1986 Transformer movie, &lt;a href="https://tfwiki.net/wiki/Unicron/Generation_1" rel="noopener noreferrer"&gt;Unicron&lt;/a&gt; was a giant, planet sized robot, also known as the God of Chaos.  &lt;/p&gt;

&lt;p&gt;For me this analogy is too obvious. DAG schedulers like Airflow (cron's), often become bloated fragile monoliths (uni-cron's).   And just like this planet eating monster, they bring to all sorts of chaos in for the engineers that maintain and operate them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foghk1ngo3yy62f0el2m9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foghk1ngo3yy62f0el2m9.jpeg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There have been quite a few great articles written on the subject of breaking up the Airflow monorepo, and to provide context I'll cover these quickly.   However, this approach alone does not defeat Unicron.  In this world of increasingly &lt;a href="https://martinfowler.com/articles/data-mesh-principles.html" rel="noopener noreferrer"&gt;decentralized data development&lt;/a&gt; we need to seriously think about using just a single scheduler.&lt;/p&gt;

&lt;h1&gt;
  
  
  Breaking up the Mono-Repo
&lt;/h1&gt;

&lt;p&gt;Airflow to often reaches limits of project dependencies, multi-team collaboration, scalability, and just overall complexity.   It's not Airflows fault, it's the way we use it.   Luckily there are a couple great approaches to solving these issues:&lt;/p&gt;

&lt;p&gt;1) Use multiple project repos - Airflow will deploy any dag you put in front of it.   So you can with a little bit of effort build a deployment pipeline which can deploy dag pipelines from separate project specific repos into a single Airflow.  There are a few techniques here ranging from DAGFactory (good article &lt;a href="https://towardsdatascience.com/airflow-design-pattern-to-manage-multiple-airflow-projects-e695e184201b" rel="noopener noreferrer"&gt;here&lt;/a&gt;), leveraging Git sub-modules, to just programmatically moving files around in your pipeline.&lt;/p&gt;

&lt;p&gt;2) Containerize your code - reduce the complexity of your Airflow project by packaging your code in separate containerized repositories.   Then use the &lt;a href="https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/stable/operators.html" rel="noopener noreferrer"&gt;pod operator&lt;/a&gt; to execute these processes.  Ideally Airflow becomes a pure orchestrator, with very simple product dependencies.   &lt;/p&gt;

&lt;p&gt;Using both of these techniques, especially in combination will help make your Unicron less formidable, perhaps only moon sized.  In fact, in many organizations, this approach coupled with a managed Airflow environment such as &lt;a href="https://aws.amazon.com/managed-workflows-for-apache-airflow/" rel="noopener noreferrer"&gt;AWS Managed Workflows&lt;/a&gt; is a really great sweet spot.&lt;/p&gt;

&lt;h1&gt;
  
  
  Data Mesh vs Unicron
&lt;/h1&gt;

&lt;p&gt;As organizations grow, and data responsibilities become more federated, we need to ask ourselves an important question - &lt;em&gt;do we really need a single scheduler?&lt;/em&gt;.   I would wholeheartedly say no, in fact it becomes a liability.   &lt;/p&gt;

&lt;p&gt;The most obvious problem - single point of failure.   Having a single scheduler, even with resiliency measures, is dangerous.   An infrastructure failure, or even a bad deployment could cause an outage for all teams.  In modern architecture we avoid SPF's if at all possible so why create one if we don't need to?&lt;/p&gt;

&lt;p&gt;Another issue is the excessive multi-team collaboration on a single project.  Possible, especially if we mitigate with the techniques above but not ideal.   You might still run into dependency issues, and of course Git conflicts.&lt;/p&gt;

&lt;p&gt;And then the most obvious question - what is the benefit?  In my experience the majority of DAG's in organization are self contained. In other words they are not using cross DAG dependencies via &lt;a href="https://airflow.apache.org/docs/apache-airflow/1.10.3/_api/airflow/sensors/external_task_sensor/index.html" rel="noopener noreferrer"&gt;External Task Sensors&lt;/a&gt;.  And if they are, there is a good chance the upstream data product is owned and maintained by another team.  So other than observing whether it is done or not, there is little utility to being in the same environment.&lt;/p&gt;

&lt;h1&gt;
  
  
  So how do we defeat Unicron?
&lt;/h1&gt;

&lt;p&gt;My recommendation is to have multiple Airflow environments, either at the team or application level.&lt;/p&gt;

&lt;p&gt;My secret sauce (well one way to accomplish this) - implement a lightweight messaging layer to communicate dependencies between the multiple Airflow environments.   The implementation details can vary - but here is a quick and simple approach: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At the end of each DAG publish to an &lt;a href="https://docs.aws.amazon.com/sns/latest/dg/welcome.html" rel="noopener noreferrer"&gt;SNS&lt;/a&gt; topic.&lt;/li&gt;
&lt;li&gt;Dependent DAGS subcribe via SQS.
&lt;/li&gt;
&lt;li&gt;The first step in the dependent DAG would then be a simple poller function (similar to an External Task Sensor), that would simply iterate and sleep until a message is received.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Obviously the implementation details are maleable and SQS could be substituted with Dynamo, Redis, or any other resilient way to notify and exchange information.  &lt;/p&gt;

&lt;p&gt;You could even have your poller run against the API of other Airflow instances. Although it will possibly couple you to another projects implementation details (i.e. specific Airflow infrastructure and DAG vs data product).  Perhaps that other team might change the DAG that builds a specific product, or replace Airflow with Prefect or maybe move to Step Functions.  In general we want to design in a way that components can evolve independently, i.e. loose coupling.&lt;/p&gt;

&lt;p&gt;One of my very first implementations of this concept was a simple client library named &lt;a href="https://github.com/equinoxfitness/datacoco-batch" rel="noopener noreferrer"&gt;Batchy&lt;/a&gt;, backed by Redis and later Dynamo.  I created this long before Data Mesh was a thing, but was guided by the same pain points highlighted above.  This simple system has been in place for years integrating multiple scheduler instances (primarily Rundeck) with little complaint and great benefit.&lt;/p&gt;

&lt;p&gt;SO - in conclusion.  Use common sense and don't create a scary, monolithic Unicron.  And if have one, be like Grimlock and Kick it's Butt. &lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhfus39zeyu5b5m0eap4x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhfus39zeyu5b5m0eap4x.jpg" alt="Me Grimlock Kick Butt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.datafutures.co/" rel="noopener noreferrer"&gt;Data Futures&lt;/a&gt;&lt;/p&gt;

</description>
      <category>airflow</category>
      <category>aws</category>
      <category>dataengineering</category>
      <category>datamesh</category>
    </item>
    <item>
      <title>Evolutionary Recommender Design with Amazon Personalize</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Tue, 29 Aug 2023 22:39:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/evolutionary-recommender-design-with-amazon-personalize-3730</link>
      <guid>https://dev.to/aws-heroes/evolutionary-recommender-design-with-amazon-personalize-3730</guid>
      <description>&lt;p&gt;Over the past few months I've been spending a fair amount of time working on personalization, leveraging one of my new favorite AWS services - &lt;a href="https://aws.amazon.com/personalize/"&gt;Amazon Personalize&lt;/a&gt;.    Needless to say there is much more that goes into building and launching a personalization system than just turning on a few services and feeding in some data.   In this article I'll focus on what it takes to launch a new personalization strategy, and architect it to evolve over time.&lt;/p&gt;

&lt;h1&gt;
  
  
  Chicken-or-the-egg
&lt;/h1&gt;

&lt;p&gt;In many cases we have a classic chicken-or-the-egg scenario - we feel uncomfortable launching personalization features without confirmed performance and perhaps limited data,  but without launching we won't have the feedback loop and data to measure and improve performance?   &lt;/p&gt;

&lt;p&gt;In some cases this is driven by the maturity and level of adoption of the application.   If we don’t include ML recommendations, we are unprepared for growth in users, but without data the ML recommendations alone may not produce relevant enough results to engage users.   In other cases we may have a mature product and user base, but still dealing with considerable unknowns.   In both cases we need to be able to experiment,  measure, and adapt quickly.&lt;/p&gt;

&lt;h1&gt;
  
  
  Launching on a New Application
&lt;/h1&gt;

&lt;p&gt;Let's consider the  special case where we are dealing with a relatively new application, in the early cycles of adoption.   In these scenarios our ML algorithms might not be producing high relevancy scores, and simple logic and/or a healthy dose of manual curation may perform better.  Note that "performing better" may be more subjective than statistical or metric driven in very early stages.   &lt;/p&gt;

&lt;p&gt;However, there are several good reason to start introducing these algorithms early&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;testing the infrastructure&lt;/strong&gt; - work out any functional or non-functional issues early on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;supplementing simple logic&lt;/strong&gt; - use ML recommendations to add variety, and reduce the chance of recommendation depletion from simple hardcoded algorithms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;being ready for scale&lt;/strong&gt; - the tipping point when ML recommendation will need to take over is somewhat unpredictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;accelerating training&lt;/strong&gt; - gathering implicit feedback from recommendation algorithms will help the models train faster&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Evolutionary Design and Candidate Sourcing
&lt;/h1&gt;

&lt;p&gt;My recommended method for integrating ML recommendations in through “candidate sourcing”.   This method will require a service layer to be built on top of the recommendation infrastructure to combine and component algorithms.    These components could be a Personalize &lt;a href="https://docs.aws.amazon.com/personalize/latest/dg/native-recipe-new-item-USER_PERSONALIZATION.html"&gt;User-Personalization&lt;/a&gt; endpoint recipe, perhaps a vector search against some text embeddings, and even a simplistic recommender based on  manually curated entries and if-then else.&lt;/p&gt;

&lt;p&gt;When a user is to be served recommendations, the underlying service will draw “candidates” for recommendations from the appropriate components , and then combine them through configured percentages/weights.&lt;/p&gt;

&lt;p&gt;For a simple example let’s consider the "for you" page  or "feed" scenario (something we are all familiar with).   In this case the API will make requests of the 3 example component services previously mentioned.   The results from each service will then be combined by configurable weights.  Let's assume 50% user-personalization, 25% vector search, and the remaining 25% from the simplistic recommender to render their feed.     &lt;/p&gt;

&lt;p&gt;Ideally we should be able to easily add a 4rd algorithm for candidate sourcing, with a configurable percentage (perhaps we include 10% popular items).      &lt;/p&gt;

&lt;h1&gt;
  
  
  Tuning, Experimentation, and Measurement
&lt;/h1&gt;

&lt;p&gt;We should be able to tune these percentage, preferably without deployment, and also run experiments (group A is 50/25/25, group B is 75/15/10).    There are many things we could use to measure the performance of these algorithms, but most simply we could measure the click thru rates by groups, as well as the component services to guide tuning and further experimentation.    Obviously, with a bit of work, we could fully automate the deployment scenarios on top of these basic principles (blue green, etc). &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.datafutures.co/"&gt;--&amp;gt; Data Futures&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Adventures in Amazon Personalize Infrastructure Deployment</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Fri, 07 Jul 2023 12:22:40 +0000</pubDate>
      <link>https://dev.to/aws-heroes/adventures-in-amazon-personalize-infrastructure-deployment-11f</link>
      <guid>https://dev.to/aws-heroes/adventures-in-amazon-personalize-infrastructure-deployment-11f</guid>
      <description>&lt;p&gt;Although I spend the majority of my time on building broad, cloud native strategies and systems, I must admit that some of my favorite work in data is quite niche – recommendation systems.   Over the past decade I've had the opportunity to build quite a few recommendation systems.   Several were the expected ecomm and media use cases, although I also had the opportunity to build in social, and even internal research systems.   &lt;/p&gt;

&lt;p&gt;These systems are rewarding in two ways: First with sufficient data and good algorithms they almost always yield good results - on one side increasing usage/revenue, and most importantly helping users find what they want.     On the other side there is a bit of both art and science.  Yes the algorithms are there, and they require selection and the technical work of training and tuning, but there is also a great bit of creativity in mapping these algorithms to user experience, combining them in interesting ways, and even planning for a bit of “fun” and surprise.&lt;/p&gt;

&lt;p&gt;In the past, building these systems was pretty heavy on the ML engineering side.  Primarily you would be leveraging OSS algorithms, and forced to build your own frameworks for training, serving, and feedback pipelines.   Not that this necessarily a horrible slog, at least for me, as building these sorts of things are fun and rewarding in their own way.  However I personally always wanted to get to the fun and creative parts.   &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And Then Amazon Personalize&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Amazon Personalize was introduced in Re:Invent of 2018, and went GA in the summer of 2019.   It started out targeting pretty basic recommendation use cases, but as it stands now in 2023 I can safely say you can build a comprehensive recommender system completely within the product.    This ranges from prepacked algorithms, through to serving and event collection infrastructure.    This gives the opportunity to skip ahead to the really fun stuff!&lt;/p&gt;

&lt;h1&gt;
  
  
  Deployment methodology
&lt;/h1&gt;

&lt;p&gt;Other than just trivial explorations, where I might use the console, I am always Infrastructure-as-code first.   Not only does this provide a repeatable way of building and tearing down infrastructure, it’s also a great way to learn about the system from the ground up.    However with Amazon Personalize, you will find that &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Personalize.html"&gt;IAC coverage&lt;/a&gt; is only for very foundational components, namely the &lt;a href="https://docs.aws.amazon.com/personalize/latest/dg/data-prep-ds-group.html"&gt;DataSet Group&lt;/a&gt; (topmost project container), &lt;a href="https://docs.aws.amazon.com/personalize/latest/dg/how-it-works-dataset-schema.html"&gt;Datasets&lt;/a&gt;, and &lt;a href="https://docs.aws.amazon.com/personalize/latest/dg/training-deploying-solutions.html"&gt;Solutions&lt;/a&gt; (a untrained model configuration).   &lt;/p&gt;

&lt;p&gt;Rest assured those smart folks at AWS didn’t forget these components, or backlog them to get an MVP out the door.   Most of the downstream components, particularly solution versions and campaigns are meant to be dynamic, and programmatically managed.   Potentially one miss being &lt;a href="https://docs.aws.amazon.com/personalize/latest/dg/recording-events.html"&gt;Event Trackers&lt;/a&gt;, which are a foundational one-time setup, and hopefully make it into CloudFormation someday soon.&lt;/p&gt;

&lt;p&gt;In an ideal fully productionalized system the flow would look something like the diagram below. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmq2lm8gfbt7to1da8pr8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmq2lm8gfbt7to1da8pr8.png" alt="Image description" width="397" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For you step function fans out there, this is an absolutely perfect use case.   A step function and several lambdas to start and poll the various Personalize API interactions would do the job nicely.  And getting back to the IAC conversation - you could absolutely IAC both the Lambdas and step functions!&lt;/p&gt;

&lt;p&gt;But what if you just want to keep it simple, and get things going quickly, or your organization is not ready to build complex step function infrastructure?&lt;/p&gt;

&lt;h1&gt;
  
  
  Management Notebook Approach
&lt;/h1&gt;

&lt;p&gt;As an engineer, I have a love-hate relationship with notebooks.  They are indeed great for prototyping and exploring data.  However notebooks are permissive of bad programming habits, and in most cases they end up being run-on-sentence type scripts.   But, they can be very helpful when used as “management scripts", and feel much less yucky.&lt;/p&gt;

&lt;p&gt;In one of my latest Personalize projects I used cloud formation for dataset groups, datasets and schemas.  I then created a management notebook for first time creation of all remaining infrastructure components, and then a re-training notebook for, you guessed it, re-training.   I’ve shared the important bits in &lt;a href="https://github.com/elliottcordo/personalize_poc"&gt;this&lt;/a&gt; repo.   Although this repo is meant to be illustrative of the approach, you could certainly customize it and use it for your own custom solution.&lt;/p&gt;

&lt;p&gt;Although the notebooks are runnable locally, I host both notebooks in Glue, and have the retraining notebook cron’d to run every hour.    And if you wanted to achieve IAC nirvana with this pragmatic solution, you could absolutely IAC the Glue notebooks.&lt;/p&gt;

&lt;h1&gt;
  
  
  So in summary..
&lt;/h1&gt;

&lt;p&gt;Personalization is awesome, unless you have a really custom/unique recommendation use, there is little reason to build a custom recommender.   Personalize is going to require a bit of work to create sustainable infrastructure deployment, definitely consider a pragmatic mix of IAC and management notebooks..   &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.datafutures.co/"&gt;https://www.datafutures.co/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>amazonpersonalize</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Deduping Customers Quick and Dirty - with SQL and Graphs</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Mon, 20 Feb 2023 20:02:06 +0000</pubDate>
      <link>https://dev.to/aws-heroes/deduping-customers-quick-and-dirty-with-sql-and-graphs-4o09</link>
      <guid>https://dev.to/aws-heroes/deduping-customers-quick-and-dirty-with-sql-and-graphs-4o09</guid>
      <description>&lt;p&gt;Just about every organization I've worked with has had some sort of data quality problem, most notably the presence of duplicate customer records.   I think we can all agree that duplicate customer records can cause all sorts of problems ranging from inconsistent customer experience to inaccurate reporting and analytics.&lt;/p&gt;

&lt;p&gt;Unfortunately many think they aren't equipped to deal with this issue.  They feel that in order to get their customer data clean they are going to need to implement some complicated and expensive piece of software like an MDM system (yuck) or a CDP (meh).   Not only do these software packages take a long time to select and implement, the customer matching capabilities are often not that impressive.  &lt;/p&gt;

&lt;p&gt;Let’s end this paralysis, roll up our sleeves and clean up our customer data with the tools we have.  My grandfather's motto was “it’s better to do something than nothing”, and to me this definitely resonates with this problem.  All you need is a good old database, preferably an MPP like Redshift, and a tiny bit of Python.   I will share an approach I've used in multiple organizations, in most cases exceeding the results from the aforementioned systems.   Although I’ve potentially thrown some shade on this approach by calling it quick and dirty, this is a perfectly reasonable, and productionizable system for large organizations as well as small and scrappy ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standardization and Matching
&lt;/h2&gt;

&lt;p&gt;The first steps in our data cleanup is to standardize our data and then essentially perform a self join to match our results together.   The theme of this article is quick and dirty, so we are going to do only very light cleanup.  After we walk through this simple but effective solution I’ll provide some tips on how to make this quite sophisticated.&lt;/p&gt;

&lt;p&gt;In the first CTE below I do some minor cleanup.  I parse the first segment of the zip and strip non numeric phone numbers.  Based on my understanding of the data I may choose to do more operations such as trimming strings or padding numbers. I also assembled a concatenated field phone_list which will allow me to compare phone numbers across a number of fields.   I could assemble a similar field if I had multiple emails or addresses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="k"&gt;select&lt;/span&gt;
 &lt;span class="n"&gt;customernumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mobile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;mobile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;phone1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;phone2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;phone3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;phone4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;nvl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mobile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;nvl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;nvl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;nvl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;nvl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;phone_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;address1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="k"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;charindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;zip&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then perform the match.   Keeping it simple, I require an exact match on the name.   I then OR together a number of conditions such as email, phone, or address.   I use &lt;code&gt;charindex&lt;/code&gt; to test the existence of a phone number in my concatenated &lt;code&gt;phone_list&lt;/code&gt; field.    I also use a &lt;code&gt;left(a.address1,5)&lt;/code&gt;  AND zip which might seem strange but in practice I've found very effective.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt;
 &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customernumber&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;matched_customernumber&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt;
 &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastname&lt;/span&gt;
 &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customernumber&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customernumber&lt;/span&gt; &lt;span class="c1"&gt;-- we don't want to join the same record to itself&lt;/span&gt;
 &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
     &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;charindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mobile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
     &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;charindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
     &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;charindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
     &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;charindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
     &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;charindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
     &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enhancing The Process
&lt;/h3&gt;

&lt;p&gt;You can put as much effort into standardization and matching as you have time to invest.    This effort will afford you a higher match rate (reducing undermatching/more dupes eliminated).  However at a certain point, especially if you make things too fuzzy you can result in overmatching (false positives/potentially merging unique people).   Given time, you have the opportunity to easily rival commercial tooling, just use your time wisely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parsing addresses&lt;/strong&gt; - standardize and break addresses into parts.  You will get a more deterministic join based on street and house number vs a string match&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve names to root forms&lt;/strong&gt; - there are many available lists and csv files out there that will resolve common abbreviations and nicknames such as the infamous Robert = Bob&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzy matching&lt;/strong&gt; - transforming field values to fuzzy representations or using fuzzy matching techniques such as Levenshtein Distance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaning bad values&lt;/strong&gt; - null out addresses and emails in your match set which appear in high frequency or match known bad values (such as a store location)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Much of the above can be done in SQL, however the beauty of platforms like Amazon Redshift is they can be &lt;a href="https://docs.aws.amazon.com/redshift/latest/dg/udf-python-language-support.html" rel="noopener noreferrer"&gt;extended with Python&lt;/a&gt;.  &lt;a href="https://towardsdatascience.com/bringing-fuzzy-matching-to-redshift-d487ce98d170" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is a great article on packaging and leveraging the Python module fuzzywuzzy within Redshift. &lt;/p&gt;

&lt;h2&gt;
  
  
  Clustering and the “Graph Trick”
&lt;/h2&gt;

&lt;p&gt;I bet a few of you have tried something similar to what i’ve outlined above and then faced a problem - how the heck do you cluster these matches and establish a survivor.  If this term “survivor” doesn’t immediately register , it essentially represents collapsing a bunch of duplicate records into a single “golden record” which will remain, and all other matched records removed.   Let’s look at a quick example to prove why this is both important as well as a difficult task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F042gjbawzswb4qer0ctc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F042gjbawzswb4qer0ctc.png" alt="duplicate records" width="800" height="103"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For simplicity, I’m only showing half of the rows in the result, as you will have the same match in a reverse relationship.   As you can see there is no natural hierarchy, and that there are transitive and multiple matches across the dataset.  The first record matches the second, the second matches the third and so on.   There is no way to easily move forward here without being able to cluster the results.  In SQL terms we want all matches group-able by a &lt;code&gt;partition by&lt;/code&gt; clause.&lt;/p&gt;

&lt;p&gt;You might be tempted to try a recursive CTE, however since this operation is designed for hierarchies and does not tolerate loops, your query will likely run infinitely and time out.  Applying limits is not a good option either as you have no control over where the query will terminate and may have incomplete results.&lt;/p&gt;

&lt;p&gt;This is where remodeling this problem as a graph can really simplify the problem.   As you can see the picture becomes a lot clearer when modeled as nodes and edges instead of rows.   And it’s not just simple for us, it’s also simpler from a programming model perspective.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedvl1axath4sntqtzlmc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedvl1axath4sntqtzlmc.png" alt="graph representation" width="792" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can now load this data into a graph model and iterate the subgraphs.  The subgraphs being the small disconnected graphs within the larger graph, and in this case just so happen to be our clusters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;networkx&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;

&lt;span class="n"&gt;Graphtype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Graph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;result.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="c1"&gt;# skip the header
&lt;/span&gt;   &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="c1"&gt;# parse into the graph
&lt;/span&gt;   &lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_edgelist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delimiter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;create_using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Graphtype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# append the individual subgraph nodes to a list
&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subgraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connected_components&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
   &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;output.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   
       &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example I assign the first customer in the flattened subgraph as survivor &lt;code&gt;(list(group)[0])&lt;/code&gt;  , but you could obviously apply any business logic that is appropriate.  Perhaps picking the lowest number, the oldest record, or maybe even importing a dataset of customer spend and choosing the customer record with the highest value or oldest/latest transaction date.  But if your trying to keep it simple, and not that familiar with Python or graphs you can use the above script as is, and resolve the rest in SQL&lt;/p&gt;

&lt;p&gt;Once you have created this dataset, you can reload it into your database and use it to create your golden record with relative ease.  You can now leverage windowed functions, coalesce, and other familiar SQL operations since we can now &lt;code&gt;partition by&lt;/code&gt; the survivor's customer number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick and Dirty?
&lt;/h2&gt;

&lt;p&gt;Quick and dirty is a good attention getter, however I actually consider this to be a pretty darn robust solution for building both one-time and recurring customer deduplication solutions.   I’d go so far as to say it represents using the right tool for the right job.  Matching and final record assembly is easily expressed in SQL, and accessible for most data wranglers to tune and improve.   Likewise clustering and survivorship almost perfectly fits the graph model.  &lt;em&gt;As a side note, I’d encourage you to keep graphs in mind when solving other types of problems as Graphs Are Everywhere..&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One other thing I wanted to mention - this solution performs!.  On a customer dataset of about 1 million records, Redshift crushed the match in under 5 seconds, and the graph clustering job in just about 2 seconds!  I've personally waited hours or even days for commercial tooling to process a full match on customer datasets of similar scale.   &lt;/p&gt;

&lt;p&gt;I hope you enjoyed this solution and are encouraged to go squash some duplicates!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.datafutures.co/" rel="noopener noreferrer"&gt;https://www.datafutures.co/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Redshift Cost Optimization with Cost Explorer</title>
      <dc:creator>elliott cordo</dc:creator>
      <pubDate>Thu, 26 Jan 2023 21:50:04 +0000</pubDate>
      <link>https://dev.to/aws-heroes/redshift-cost-optimization-with-cost-explorer-382f</link>
      <guid>https://dev.to/aws-heroes/redshift-cost-optimization-with-cost-explorer-382f</guid>
      <description>&lt;p&gt;These days, cloud efficiency and cost savings are top of mind for many organizations.  The current economic conditions aside, it’s always a good opportunity to support efforts to use the cloud efficiently.   Besides saving money, the same levers that drive efficiency very often directly support scalability, reliability and sustainability.   &lt;/p&gt;

&lt;p&gt;Data Warehouses were historically statically provisioned, fixed cost systems.  The good news is that advancements in cloud native data warehouse platforms have enabled us to maximize efficiency and cost much like our other engineered systems.  &lt;/p&gt;

&lt;p&gt;There are obviously a lot of different ways that we can approach efficiency.   Optimizing a system like Redshift should include both infrastructure configuration as well as what “runs on” the platform.  The latter may include various improvements such as optimizing queries, table structures and materialization patterns, or maybe even moving some of the workloads outside the data warehouse platform (ex. moving big crunches to Elastic Map Reduce).   For this article I’ll focus on the infrastructure layer, using only insights from Cost Explorer and assume what runs on Redshift is fixed.&lt;/p&gt;

&lt;p&gt;As many of you know, Redshift RA3 instance types decouple compute and storage.  Furthermore they have built elasticity into compute, allowing you to handle spikes and increases in workload by leveraging features such as Concurrency Scaling and Serverless Endpoints.   These features, along with the ability to blend provisioned with serverless and elastic resources is why Redshift delivers such excellent price-performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Explorer to the rescue!
&lt;/h2&gt;

&lt;p&gt;As the old saying goes “you can't improve what you don't measure”.   So the first step in figuring out whether your Redshift infrastructure is optimal is reviewing Cost Explorer.    This is not necessarily a trivial task as there are many components to the actual billing, with different timing, and unfortunately some cryptic abbreviations.  &lt;/p&gt;

&lt;p&gt;Within cost explorer change the report parameters to Granularity: Monthly (this is a good place to start), Dimension: Usage type, and set a Filter on Service: Redshift.   You will then end up with a report that looks something like below.     This example infrastructure configuration is really useful, as it demonstrates nearly all the components you may see with a standard RA3 and serverless deployment.    I’ll now walk through the line items, and identify where there may be some efficiency opportunities.   &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqb6ptk73v8eznmmo1wia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqb6ptk73v8eznmmo1wia.png" alt="cost explorer example" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s get familiar with the abbreviations and their meanings.   All the pricing details can be found &lt;a href="https://aws.amazon.com/redshift/pricing/" rel="noopener noreferrer"&gt;here&lt;/a&gt;, but I’ll try to summarize the important bits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;USE-ServerlessUsage&lt;/strong&gt; - The cost of the used Redshift Processing Units (this includes the whatever base capacity you have reserved plus the associated actual billing above that capacity)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HeavyUsage&lt;/strong&gt; - Redshift reserved instance cost (be sure to select the 1st of the month in your date selection to pick up this line item)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node&lt;/strong&gt; -  Redshift on-demand usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RMS&lt;/strong&gt; - Redshift managed storage, storage cost in GB hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CS&lt;/strong&gt; - Concurrency Scaling, you accrue up to one hour of concurrency scaling per day, usage beyond this is billed per-second on-demand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PaidSnapshots&lt;/strong&gt; - Backups, necessary of course but definitely not free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;USE-DataScanned&lt;/strong&gt; - Redshift spectrum usage, querying data that exists in S3 or other external sources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Findings
&lt;/h2&gt;

&lt;p&gt;For context we are looking at 3 pieces of Redshift infrastructure: a reserved ra3.4xlarge cluster, an on-demand ra3.xlplus cluster, and a Redshift Serverless endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Excessive Concurrency Scaling&lt;/strong&gt;&lt;br&gt;
Our Concurrency Scaling (CS:ra3.4xlarge) is approaching the cost of our reserved cluster.   This cluster is obviously blowing through the daily budget, and relying heavily on on-demand capacity to complete its computing tasks.    First off, let’s acknowledge how cool this is - we are running a cluster at or &lt;em&gt;above&lt;/em&gt; the redline and still completing the necessary work and serving our end users.    A cost effective solution here is to offset the on-demand pricing with reserved instances.   A good experiment would be to add an on-demand node to the cluster and observe the reduction in Concurrency Scaling and overall cluster CPU.    If the calculated price profile is favorable consider reserving nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redshift Serverless Usage&lt;/strong&gt;&lt;br&gt;
The Redshift Serverless endpoint (USE1-ServerlessUsage) costs have eclipsed the costs of our reserved cluster.    There may be good reasons for this, and it’s possible that this is the most cost effective way of handling this workload.    The first thing to do is to check the Base RPU settings for this cluster, making sure that we haven’t staked a baseline commitment that is too high, and does not reflect the usage and minimum requirements of the cluster.    Depending on the actual RPU units consumed and usage patterns this may mean that a reserved provisioned cluster might be more effective.   A good common sense rule, at this point in time, is that a highly utilized cluster will be more cost effective staying provisioned.  Note that the math here is a little tricky and there is no official guidance just yet, but let’s use 60% avg cluster CPU as a good case to keep provisioned.   I suggest reading &lt;a href="https://aws.amazon.com/redshift/pricing/" rel="noopener noreferrer"&gt;this&lt;/a&gt; section of the documentation on monitoring cost and usage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Demand Nodes&lt;/strong&gt;&lt;br&gt;
The ra3.xlplus cluster is contributing a fair amount of billing given its performance capacity.    It’s easy to fall into the trap of running on-demand too long, especially when there are pending changes to the environment.    Another common sense rule - if you plan on running this infrastructure more than 6 months you probably want to do a 1 year reserve, and greater than 18 months do a 3 year.    We humans are pretty poor planners in general, especially when making guesses with uncertainty.   In my experience the rules above have always been net favorable.   Even if your guess is correct you’ll be pretty close to the break even point of the reserve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshots&lt;/strong&gt;&lt;br&gt;
The snapshot (Redshift:PaidSnapshots) costs are a little high, but reasonable.  However, it is still worth an investigation.   Start by reviewing your retention policy, and make sure that it complies with your organization's service levels and policies.   Also be sure to page back to the early history and make sure you're not permanently safekeeping a large number of final snapshots.    I’ve seen large buildups of these from misconfigured CICD pipelines or programmatic restores to non-prod.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Closing
&lt;/h2&gt;

&lt;p&gt;Optimizing infrastructure can be both a fun and rewarding exercise.   It can be like playing Sherlock Holmes with Cost Explorer as your Watson.  Based solely on these cost explorer findings there is a relatively easy 10 to 20% cost savings through infrastructure configurations.   Although this post outlines high level infrastructure review, there is definitely a lot more digging to do, especially with what “runs on” Redshift.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;em&gt;Go forth and be frugal.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.datafutures.co/" rel="noopener noreferrer"&gt;https://www.datafutures.co/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devmeme</category>
      <category>watercooler</category>
    </item>
  </channel>
</rss>
