<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>SNS &#8211; Blog of Kliment Andreev &#8211; A place so I won&#039;t forget things</title>
	<atom:link href="https://blog.andreev.it/tag/sns/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.andreev.it</link>
	<description></description>
	<lastBuildDate>Thu, 05 Nov 2020 17:31:50 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>AWS: Get SES bounce, complaint, delivery details using Lambda, SNS and DynamoDB or AWS Glue</title>
		<link>https://blog.andreev.it/2019/08/aws-get-ses-bounce-complaint-delivery-details-using-lambda-sns-and-dynamodb-or-aws-glue/</link>
					<comments>https://blog.andreev.it/2019/08/aws-get-ses-bounce-complaint-delivery-details-using-lambda-sns-and-dynamodb-or-aws-glue/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Wed, 07 Aug 2019 18:07:47 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[DynamoDB]]></category>
		<category><![CDATA[Glue]]></category>
		<category><![CDATA[Lambda]]></category>
		<category><![CDATA[SES]]></category>
		<category><![CDATA[SMTP]]></category>
		<category><![CDATA[SNS]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=5513</guid>

					<description><![CDATA[Recently, I had to deal with a lot of bounced e-mails from a bunch&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>Recently, I had to deal with a lot of bounced e-mails from a bunch of websites that we manage and send daily newsletters, order updates etc. These sites use AWS SES to send e-mails using the AWS SMTP server. Unfortunately for me, the developers didn&#8217;t have any logging enabled and different sites are managed by different developers in different countries. So, the easiest way was to get the logs from the SMTP server itself. But, how? At the time of this writing, you can&#8217;t just get the SMTP logs from SES like any other SMTP server. It&#8217;s a multi-step process. Fortunately, AWS provided a tutorial for this. The idea is to enable notifications on each SES domain that sends a notification to a SNS topic anytime there is a bounce, complaint or delivery. This SNS topic then triggers a Lambda function in Node.js that populates a DynamoDB table with the information that you need. Here is the <a href="https://aws.amazon.com/premiumsupport/knowledge-center/lambda-sns-ses-dynamodb/" rel="noopener noreferrer" target="_blank">link </a>to AWS tutorial and I strongly suggest to watch the video. It goes into much more details. <strong>BUT!</strong> This approach works fine if you have to deal with several domains and bounces per day. But if you have to deal with 50+ domains and over 10K bounced emails a day, you&#8217;ll have to automate the way of handling the results.<br />
So, I&#8217;ll show you how to implement this solution using the AWS approach with some changes in the script and then we&#8217;ll use some tools to export the DynamoDB table on a daily basis to whoever is managing the cleanups of the bounced e-mails. An optional part of this post will be to go full serverless and instead of using a server with parsing tools, we&#8217;ll use AWS Glue to export the DynamoDB to an S3 bucket.</p>
<h1>SNS topic and subscription</h1>
<p>We&#8217;ll use SNS topic and SNS subscription to trigger the SES notifications. Go to SNS and create a topic. I named mine <strong>snsSESLogs</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-01.jpg"><img fetchpriority="high" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-01.jpg" alt="" width="816" height="389" class="aligncenter size-full wp-image-8641" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-01.jpg 816w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-01-300x143.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-01-768x366.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-01-585x279.jpg 585w" sizes="(max-width: 816px) 100vw, 816px" /></a><br />
Once completed, create a subscription.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-02.jpg"><img decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-02.jpg" alt="" width="721" height="370" class="aligncenter size-full wp-image-8642" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-02.jpg 721w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-02-300x154.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-02-585x300.jpg 585w" sizes="(max-width: 721px) 100vw, 721px" /></a><br />
We&#8217;ll use a temporarily e-mail subscription that notifies us for any SES activity. You can leave this notification as permanent as you want, but mind that you&#8217;ll receive an email for EVERY SES event (delivery, bounce or complaint). I don&#8217;t want to receive thousands of e-mails a day, so I&#8217;ll use this only for the purpose of troubleshooting. Under <strong>Topic ARN</strong>, select the topic that you just created, choose <strong>Email </strong>for the <strong>Protocol </strong>and type your e-mail address under <strong>Endpoint</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-03.jpg"><img decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-03.jpg" alt="" width="420" height="379" class="aligncenter size-full wp-image-8643" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-03.jpg 420w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-03-300x271.jpg 300w" sizes="(max-width: 420px) 100vw, 420px" /></a><br />
Make sure you confirm your subscription by checking the e-mail address that you used for this subscription. You&#8217;ll receive an e-mail from AWS telling you to click the link in the e-mail and confirm.</p>
<h1>SES</h1>
<p>Now, go to SES and click on the domain that you have there. Find the <strong>Notifications </strong>section and click on <strong>Edit Configuration</strong> button at the bottom. Depending on what do you want logged, select the SNS topic that you just created. Most likely, you won&#8217;t want to see the delivery confirmations. You can still enable it here and then make a change in the script. The idea is to configure the SES domain, so anytime there is an e-mail going thru, a SNS notification is generated.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-04.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-04.jpg" alt="" width="871" height="435" class="aligncenter size-full wp-image-8644" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-04.jpg 871w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-04-300x150.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-04-768x384.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-04-585x292.jpg 585w" sizes="(max-width: 871px) 100vw, 871px" /></a></p>
<h1>DynamoDB</h1>
<p>Go to DynamoDB and click on <strong>Create table</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-05.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-05.png" alt="" width="941" height="133" class="aligncenter size-full wp-image-8645" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-05.png 941w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-05-300x42.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-05-768x109.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-05-585x83.png 585w" sizes="(max-width: 941px) 100vw, 941px" /></a><br />
Name the table however you want, but keep in mind that you have to reference the name later. Add a primary key and a sort key. Also, name them whatever you want, but you have to reference them later. All these are case sensitive.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-06.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-06.png" alt="" width="608" height="296" class="aligncenter size-full wp-image-8646" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-06.png 608w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-06-300x146.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-06-585x285.png 585w" sizes="(max-width: 608px) 100vw, 608px" /></a><br />
Use the defaults for the rest and click <strong>Create</strong>. </p>
<h2>TTL (time-to-live) for items</h2>
<p>Here is the first change that differs from the AWS tutorial above. In their tutorial, the records are there indefinitely. We don&#8217;t want that. All we care is to get the logs, parse them, deliver them and delete them. If you <strong>DO </strong>care about the logs for some compliance reason, skip this step. You&#8217;ll probably have to modify the parsing script later, but we&#8217;ll worry about that later. For now, let&#8217;s configure the <strong>TTL</strong>. In essence, this option tells AWS backend to delete certain records after <em>whatever_you_choose</em> hours or so. Read <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html" rel="noopener noreferrer" target="_blank">this</a>, so you have an understanding of what&#8217;s going on. AWS doesn&#8217;t guarantee that it will delete those records/items exactly after <em>whatever_you_choose</em> hours, so you still have to do some filtering. I&#8217;ll talk about that later.<br />
Go to the <strong>Overview </strong>tab of your table and click on <strong>Manage TTL</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-07.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-07.png" alt="" width="674" height="228" class="aligncenter size-full wp-image-8647" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-07.png 674w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-07-300x101.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-07-585x198.png 585w" sizes="(max-width: 674px) 100vw, 674px" /></a><br />
Type <strong>TTL </strong>, leave the rest as is and click <strong>Continue</strong>. <strong>NOTE</strong>: Everything is case sensitive.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-08.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-08.png" alt="" width="790" height="569" class="aligncenter size-full wp-image-8648" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-08.png 790w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-08-300x216.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-08-768x553.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-08-585x421.png 585w" sizes="(max-width: 790px) 100vw, 790px" /></a></p>
<h1>IAM</h1>
<p>We&#8217;ll need a role and a policy to restrict access to the solution that we are implementing. Go to <strong>IAM </strong>console and click on <strong>Policies</strong>. Click on <strong>Create policy</strong> and click on the <strong>JSON </strong>tab. Delete the placeholder and paste this text. Make sure you replace the account number with yours and the name of the DynamoDB table that you just created. Don&#8217;t add the <strong><></strong> pair.</p>
<pre class="brush: xml; highlight: [11]; title: ; notranslate">
{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: &#x5B;
        {
            &quot;Sid&quot;: &quot;Stmt1428510662000&quot;,
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;DynamoDB:PutItem&quot;
            ],
            &quot;Resource&quot;: &#x5B;
                &quot;arn:aws:dynamodb:us-east-1:&lt;account_number&gt;:table/&lt;DynamoDB_table_name&gt;&quot;
            ]
        }
    ]
}
</pre>
<p>Click on <strong>Review policy</strong> and enter a <strong>Name </strong>and a <strong>Description</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-09.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-09.png" alt="" width="1255" height="510" class="aligncenter size-full wp-image-8649" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-09.png 1255w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-09-300x122.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-09-1024x416.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-09-768x312.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-09-1170x475.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-09-585x238.png 585w" sizes="(max-width: 1255px) 100vw, 1255px" /></a><br />
Click on <strong>Create policy</strong> after.<br />
The next step is to create an AWS role. Click on the <strong>Roles </strong>on the left and then click on <strong>Create role</strong>. Choose <strong>AWS service</strong> and then <strong>Lambda</strong>, then click <strong>Next: Permissions</strong> at the bottom.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-10.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-10.png" alt="" width="727" height="379" class="aligncenter size-full wp-image-8650" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-10.png 727w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-10-300x156.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-10-585x305.png 585w" sizes="(max-width: 727px) 100vw, 727px" /></a><br />
Under <strong>Filter policies</strong>, start typing <strong>AWSLambda </strong>and choose the <strong>AWSLambdaBasicExecutionRole</strong>. Put a check mark next to it.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-11.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-11.png" alt="" width="988" height="405" class="aligncenter size-full wp-image-8651" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-11.png 988w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-11-300x123.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-11-768x315.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-11-585x240.png 585w" sizes="(max-width: 988px) 100vw, 988px" /></a><br />
Before you click <strong>Next: Tags</strong>, type <strong>polSESL </strong>(or whatever you named your policy) under <strong>Filter policies</strong> again. Put a check mark to this policy and click on <strong>Next: Tags</strong>. The idea is to attach our policy for DynamoDB but also the policy for the execution of Lambda.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-12.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-12.png" alt="" width="997" height="356" class="aligncenter size-full wp-image-8652" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-12.png 997w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-12-300x107.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-12-768x274.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-12-585x209.png 585w" sizes="(max-width: 997px) 100vw, 997px" /></a><br />
Tag it if you want and proceed with <strong>Next: Review</strong>.<br />
Enter the <strong>Role name</strong>, <strong>Role description</strong> (if you want) and click on <strong>Create role</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-13.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-13.png" alt="" width="1020" height="615" class="aligncenter size-full wp-image-8653" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-13.png 1020w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-13-300x181.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-13-768x463.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-13-585x353.png 585w" sizes="(max-width: 1020px) 100vw, 1020px" /></a></p>
<h1>Lambda</h1>
<p>It&#8217;s time for our Lambda function. I made some changes to the Node.js script that was provided by AWS. The original script doesn&#8217;t log the sender&#8217;s domain/e-mail. Also, the original script doesn&#8217;t deal with the TTL attribute that we added. So, go to <strong>Lambda </strong>and click on <strong>Create function</strong>.<br />
Use the following. Name your function, choose <strong>Node.js 8.10</strong> for the <strong>Runtime</strong>, choose <strong>Use an existing role</strong> and select the role that we just created.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-14.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-14.png" alt="" width="935" height="791" class="aligncenter size-full wp-image-8654" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-14.png 935w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-14-300x254.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-14-768x650.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-14-585x495.png 585w" sizes="(max-width: 935px) 100vw, 935px" /></a><br />
Click on <strong>Create function</strong> after.<br />
Switch to <strong>Node.js 4.3</strong> under <strong>Runtime </strong>and delete the placeholder code under <strong>index.js</strong>. NOTE: I am not <strong>Node.js</strong> expert and I am not sure if the code will work under <strong>Node.js 8+</strong> and above. I didn&#8217;t have time to test it.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-15.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-15.png" alt="" width="952" height="304" class="aligncenter size-full wp-image-8655" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-15.png 952w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-15-300x96.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-15-768x245.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-15-585x187.png 585w" sizes="(max-width: 952px) 100vw, 952px" /></a><br />
Paste this Node.js code. If you change the names of the DynamoDB table and the keys, make sure you change the names in the highlighted lines. The number &#8220;24&#8221; on line 7 means 24 hours. That&#8217;s my time-to-live. If you need to keep your records more or less, change this accordingly. If you don&#8217;t want to register the successful deliveries in DynamoDB comment line 40. Add <strong>//</strong> as prefix.</p>
<pre class="brush: jscript; highlight: [3,7,8,13,21,37,40,53]; title: ; notranslate">
console.log('Loading event');
var aws = require('aws-sdk');
var ddb = new aws.DynamoDB({params: {TableName: 'ddbSESLogs'}});
exports.handler = function(event, context)
{
  console.log('Received event:', JSON.stringify(event, null, 2));
  const TTL = Math.round(Date.now() / 1000) + 24 * 3600;
  var SnsPublishTime = event.Records&#x5B;0].Sns.Timestamp
  var SnsTopicArn = event.Records&#x5B;0].Sns.TopicArn;
  var SESMessage = event.Records&#x5B;0].Sns.Message
  SESMessage = JSON.parse(SESMessage);
  var SESMessageType = SESMessage.notificationType;
  var SESMessageId = SESMessage.mail.messageId;
  var SESDestinationAddress = SESMessage.mail.destination.toString();
  var SESSourceAddress = SESMessage.mail.source.toString();
  var LambdaReceiveTime = new Date().toString();
  if (SESMessageType == 'Bounce')
  {
    var SESreportingMTA = SESMessage.bounce.reportingMTA;
    var SESbounceSummary = JSON.stringify(SESMessage.bounce.bouncedRecipients);
    var itemParams = {Item: {SESMessageId: {S: SESMessageId}, SnsPublishTime: {S: SnsPublishTime}, TTL: {N: TTL.toString()},
      SESreportingMTA: {S: SESreportingMTA}, SESSourceAddress: {S: SESSourceAddress}, 
      SESDestinationAddress: {S: SESDestinationAddress}, SESbounceSummary: {S: SESbounceSummary}, SESMessageType: {S: SESMessageType}}};
    ddb.putItem(itemParams, function(err, data)
    {
      if(err) { context.fail(err)}
      else {
         console.log(data);
         context.succeed();
      }
    });
  }
  else if (SESMessageType == 'Delivery')
  {
    var SESsmtpResponse1 = SESMessage.delivery.smtpResponse;
    var SESreportingMTA1 = SESMessage.delivery.reportingMTA;
    var itemParamsdel = {Item: {SESMessageId: {S: SESMessageId}, SnsPublishTime: {S: SnsPublishTime}, TTL: {N: TTL.toString()}, SESsmtpResponse: {S: SESsmtpResponse1},
      SESreportingMTA: {S: SESreportingMTA1}, SESSourceAddress: {S: SESSourceAddress },
      SESDestinationAddress: {S: SESDestinationAddress }, SESMessageType: {S: SESMessageType}}};
    ddb.putItem(itemParamsdel, function(err, data)
    {
      if(err) { context.fail(err)}
      else {
        console.log(data);
        context.succeed();
      }
    });
  } 
  else if (SESMessageType == 'Complaint')
  {
    var SESComplaintFeedbackType = SESMessage.complaint.complaintFeedbackType;
    var SESFeedbackId = SESMessage.complaint.feedbackId;
    var itemParamscomp = {Item: {SESMessageId: {S: SESMessageId}, SnsPublishTime: {S: SnsPublishTime}, TTL: {N: TTL.toString()}, SESComplaintFeedbackType: {S: SESComplaintFeedbackType},
      SESFeedbackId: {S: SESFeedbackId}, SESSourceAddress: {S: SESSourceAddress },
      SESDestinationAddress: {S: SESDestinationAddress }, SESMessageType: {S: SESMessageType}}};
    ddb.putItem(itemParamscomp, function(err, data)
    {
      if(err) { context.fail(err)}
      else {
        console.log(data);
        context.succeed();
      }
    });
  }
};
</pre>
<p>Click <strong>Save </strong>in the upper-right corner once you are done and then click on <strong>Add trigger</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-16.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-16.jpg" alt="" width="1102" height="389" class="aligncenter size-full wp-image-8656" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-16.jpg 1102w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-16-300x106.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-16-1024x361.jpg 1024w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-16-768x271.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-16-585x207.jpg 585w" sizes="(max-width: 1102px) 100vw, 1102px" /></a><br />
Configure the trigger so it looks like this. The SNS topic that we created should be the trigger.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-17.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-17.jpg" alt="" width="825" height="539" class="aligncenter size-full wp-image-8657" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-17.jpg 825w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-17-300x196.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-17-768x502.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-17-585x382.jpg 585w" sizes="(max-width: 825px) 100vw, 825px" /></a><br />
Click <strong>Add</strong>. At this point we are pretty much done with the AWS part.</p>
<h1>Test</h1>
<p>To check if everything works fine, send an e-mail from one of your domains that you configured to a non-existing e-mail address and existing domain (e.g. <strong>test@iandreev.com</strong>). If everything is right, you&#8217;ll see a record in DynamoDB.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-18.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-18.jpg" alt="" width="755" height="407" class="aligncenter size-full wp-image-8658" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-18.jpg 755w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-18-300x162.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-18-585x315.jpg 585w" sizes="(max-width: 755px) 100vw, 755px" /></a><br />
Scroll to the right and you&#8217;ll see our <strong>TTL </strong>attribute. Hover your mouse over it and you&#8217;ll see when is the record set to expire. I did my test on August 3rd, 6:30ish AM and it tells me that the record will expire in 24 hrs. Remember line 7 in the Node.js Lambda script? We put 24 there.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-19.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-19.jpg" alt="" width="534" height="133" class="aligncenter size-full wp-image-8659" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-19.jpg 534w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-19-300x75.jpg 300w" sizes="(max-width: 534px) 100vw, 534px" /></a><br />
If something is wrong and you are not seeing the expected result, go to <strong>Cloudwatch </strong>and under <strong>Logs</strong>, choose the logs for your Lambda function.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-20.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-20.jpg" alt="" width="534" height="271" class="aligncenter size-full wp-image-8660" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-20.jpg 534w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-20-300x152.jpg 300w" sizes="(max-width: 534px) 100vw, 534px" /></a></p>
<h1>Parsing</h1>
<p>You can export the records from DynamoDB from the console as CSV file. Unfortunately, if you want to export the table from command line using <a href="https://blog.andreev.it/?p=1905" rel="noopener noreferrer" target="_blank">AWS CLI</a>, you can&#8217;t. You can only get the output as text or JSON.</p>
<pre class="brush: bash; title: ; notranslate">
aws dynamodb scan --table-name ddbSESLogs --query &quot;Items&#x5B;*]&quot; --output json
</pre>
<p>In order to convert it to CSV, we&#8217;ll use a tool called <strong>jq</strong>. You can get the tool from <a href="https://stedolan.github.io/jq/" rel="noopener noreferrer" target="_blank">here</a>. Create this file first and save it as <strong>json2csv.jq</strong>.</p>
<pre class="brush: jscript; title: ; notranslate">
def json2header:
  &#x5B;paths(scalars) | join(&quot;.&quot;)];

def json2array($header):
  json2header as $h
  | if $h == $header
    then &#x5B;paths(scalars) as $p | getpath($p)]
    else &quot;headers do not match: \($header) vs \($h)&quot; | error
    end ;

# given an array of conformal objects, produce &quot;CSV&quot; rows, with a header row:
def json2csv:
  (.&#x5B;0] | json2header) as $h
  | ($h, (.&#x5B;] | json2array($h)))
  | @csv ;

# `main`
json2csv
</pre>
<p>Now, you can pipe the output of the JSON file to jq.</p>
<pre class="brush: bash; title: ; notranslate">
aws dynamodb scan --table-name ddbSESLogs --query &quot;Items&#x5B;*]&quot; --output json | jq -rf json2csv.jq &gt; output.csv
</pre>
<p>If you open output.csv, you&#8217;ll see that you have all your records there. But, here is the problem. Let&#8217;s say that you schedule the command above to run every 24 hours. You would think that AWS will delete the expired records that are older than 24 hours. But, they don&#8217;t guarantee that. So, we have to do the filtering. We&#8217;ll download whatever is in the table and then parse the input to filter records that are within 24 hour range.<br />
Let&#8217;s create these two files. Name them <strong>names.json</strong> and <strong>values.json</strong>. If you named your <strong>TTL </strong>attribute differently or in lower case, you have to change the values below.<br />
<strong>names.json</strong></p>
<pre class="brush: xml; title: ; notranslate">
{
  &quot;#t&quot;: &quot;TTL&quot;
}
</pre>
<p><strong>values.json</strong></p>
<pre class="brush: xml; title: ; notranslate">
{
    &quot;:TTL&quot;:
     {
       &quot;N&quot;: &quot;1564502762&quot;
     }
}
</pre>
<p>The value <strong>1564502762 </strong>is an Epoch date. <em>The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z).</em><br />
The idea is to inject an Epoch value in <strong>values.json</strong> that is 24 hours in the past, so when we run the command to filter, we&#8217;ll get the records that are 24 hours or less old.<br />
This is the command that will filter the DynamoDB table.</p>
<pre class="brush: bash; title: ; notranslate">
aws dynamodb scan --table-name ddbSESLogs --query &quot;Items&#x5B;*]&quot; --output json --filter-expression &quot;#t &gt; :TTL&quot; \
--expression-attribute-names file://names.json --expression-attribute-values file://values.json
</pre>
<p>If you run the command as-is, you&#8217;ll get all the records. Why? Because <strong>1564502762 </strong>is in the past. It&#8217;s July 30th, 4PM. Use <a href="https://www.epochconverter.com/" rel="noopener noreferrer" target="_blank">this </a>site to convert between human readable output and Epoch Unix time. To get an idea of what&#8217;s going on, here are my TTL values for the latest records.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-21.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-21.png" alt="" width="125" height="215" class="aligncenter size-full wp-image-8661" /></a><br />
If I change the value in <strong>values.json</strong> to be 1564831295, I&#8217;ll get the last record from the set and nothing more. The before-last record won&#8217;t be included because I am using greater than, not greater or equal than comparison in the line below.</p>
<pre class="brush: bash; title: ; notranslate">
aws dynamodb scan --table-name ddbSESLogs --query &quot;Items&#x5B;*]&quot; --output json --filter-expression &quot;#t &gt; :TTL&quot; \
--expression-attribute-names file://names.json --expression-attribute-values file://values.json
</pre>
<p>So, before I do a comparison, I have to change the value in <strong>values.json</strong> and make it 24 hours ago. Make sure the JSON format of values.json is exactly the same as my example. </p>
<pre class="brush: bash; title: ; notranslate">
sed -i &quot;s/.*&#x5B;\&quot;]N&#x5B;\&quot;]:.*/$(echo -e &quot;\t&quot;'&quot;N&quot;: &quot;'$(echo &quot;`date +%s` -3600*24&quot;|bc)'&quot;')/&quot; values.json
</pre>
<p>The command above when executed will change the value in <strong>values.json</strong> for 24 hours in the past when executed. Now, you can combine these two in a small script and schedule it in a cron. You can expand the script so it sends the result to S3 or in an e-mail to whoever is responsible. Make sure the paths are correct for sed, aws and json files.</p>
<pre class="brush: bash; title: ; notranslate">
#!/bin/sh
/usr/bin/sed -i &quot;s/.*&#x5B;\&quot;]N&#x5B;\&quot;]:.*/$(echo -e &quot;\t&quot;'&quot;N&quot;: &quot;'$(echo &quot;`date +%s` -3600*24&quot;|bc)'&quot;')/&quot; /somewhere/values.json
/usr/bin/aws dynamodb scan --table-name ddbSESLogs --query &quot;Items&#x5B;*]&quot; --output json --filter-expression &quot;#t &gt; :TTL&quot; \
--expression-attribute-names file://somewhere/names.json \
--expression-attribute-values file://somewhere/values.json &gt; /somewhere/output.csv
</pre>
<h1>AWS Glue (optional)</h1>
<p>If you don&#8217;t want to deal with a Linux server, AWS CLI and jq, then you can use <strong>AWS Glue</strong>. This AWS ETL service will allow you to run a job (scheduled or on-demand) and send your DynamoDB table to an S3 bucket. It&#8217;s up to you what you want to do with the files in the bucket. You might want to keep them indefinitely, move them to Glacier or just expire them after some time. First, create an S3 bucket and name it <strong>aws-glue-something</strong>. The reason I&#8217;ll name the bucket like this is because AWS Glue will create its own policy and this policy have write access to all aws-glue-* buckets. So, instead of naming my bucket whatever I want and then attach extra policy, I&#8217;ll use only a single policy.<br />
Then, go to <strong>AWS Glue</strong> and click on <strong>Databases </strong>from top left. Then click <strong>Add database</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-22.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-22.png" alt="" width="423" height="203" class="aligncenter size-full wp-image-8662" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-22.png 423w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-22-300x144.png 300w" sizes="(max-width: 423px) 100vw, 423px" /></a><br />
Name the database (in my case <strong>gdbSESLogs</strong>) and click on <strong>Create</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-23.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-23.png" alt="" width="531" height="258" class="aligncenter size-full wp-image-8663" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-23.png 531w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-23-300x146.png 300w" sizes="(max-width: 531px) 100vw, 531px" /></a><br />
Click on <strong>Tables </strong>below <strong>Databases </strong>and click <strong>Add tables</strong>, then <strong>Add tables using a crawler</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-24.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-24.png" alt="" width="455" height="205" class="aligncenter size-full wp-image-8664" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-24.png 455w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-24-300x135.png 300w" sizes="(max-width: 455px) 100vw, 455px" /></a><br />
I&#8217;ll name my crawler <strong>craSESLogs</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-25.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-25.png" alt="" width="681" height="220" class="aligncenter size-full wp-image-8665" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-25.png 681w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-25-300x97.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-25-585x189.png 585w" sizes="(max-width: 681px) 100vw, 681px" /></a><br />
Choose <strong>Data stores</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-26.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-26.png" alt="" width="408" height="175" class="aligncenter size-full wp-image-8666" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-26.png 408w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-26-300x129.png 300w" sizes="(max-width: 408px) 100vw, 408px" /></a><br />
Choose a <strong>DynamoDB </strong>data store and select your DynamoDB table.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-27.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-27.png" alt="" width="648" height="257" class="aligncenter size-full wp-image-8667" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-27.png 648w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-27-300x119.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-27-585x232.png 585w" sizes="(max-width: 648px) 100vw, 648px" /></a><br />
Don&#8217;t add any additional data stores and click <strong>Next</strong>.<br />
Allow AWS to create an IAM role for you. Just name it.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-28.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-28.png" alt="" width="667" height="456" class="aligncenter size-full wp-image-8668" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-28.png 667w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-28-300x205.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-28-585x400.png 585w" sizes="(max-width: 667px) 100vw, 667px" /></a><br />
You can choose how do you want to schedule the crawler. Pick the <strong>Run on demand</strong> option so you can test. You can always modify the crawler later and schedule it.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-29.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-29.png" alt="" width="656" height="195" class="aligncenter size-full wp-image-8669" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-29.png 656w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-29-300x89.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-29-585x174.png 585w" sizes="(max-width: 656px) 100vw, 656px" /></a><br />
Select the <strong>Glue </strong>database that you&#8217;ve created initially.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-30.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-30.png" alt="" width="670" height="446" class="aligncenter size-full wp-image-8670" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-30.png 670w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-30-300x200.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-30-585x389.png 585w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-30-263x175.png 263w" sizes="(max-width: 670px) 100vw, 670px" /></a><br />
Finally, click <strong>Finish</strong>. Now that you have Glue database, table and crawler ready, run the crawler so it takes the data from DynamoDB and populates the Glue table.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-31.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-31.png" alt="" width="492" height="171" class="aligncenter size-full wp-image-8671" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-31.png 492w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-31-300x104.png 300w" sizes="(max-width: 492px) 100vw, 492px" /></a><br />
Once the crawler completes, from the left-side menu under <strong>ETL </strong>sub-menu choose <strong>Jobs</strong> and click <strong>Add job</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-32.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-32.png" alt="" width="364" height="261" class="aligncenter size-full wp-image-8672" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-32.png 364w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-32-300x215.png 300w" sizes="(max-width: 364px) 100vw, 364px" /></a><br />
Name the job, select the role you&#8217;ve created earlier and leave the rest as-is on my screenshot.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-33.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-33.png" alt="" width="675" height="635" class="aligncenter size-full wp-image-8673" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-33.png 675w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-33-300x282.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-33-585x550.png 585w" sizes="(max-width: 675px) 100vw, 675px" /></a><br />
For the <strong>data source</strong>, choose the DynamoDB table.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-34.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-34.png" alt="" width="965" height="115" class="aligncenter size-full wp-image-8674" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-34.png 965w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-34-300x36.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-34-768x92.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-34-585x70.png 585w" sizes="(max-width: 965px) 100vw, 965px" /></a><br />
For the <strong>data target</strong>, choose to <strong>Create tables in your data target</strong>. Choose <strong>S3 </strong>for the <strong>Data store</strong>, <strong>CSV </strong>as <strong>Format </strong>and choose the bucket where the exported file will end as below. NOTE: Make sure your IAM role allows write access to the S3 bucket.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-35.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-35.png" alt="" width="482" height="484" class="aligncenter size-full wp-image-8675" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-35.png 482w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-35-300x300.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-35-150x150.png 150w" sizes="(max-width: 482px) 100vw, 482px" /></a><br />
You can change the mappings or accept the defaults as I did. For example, if you don&#8217;t like some columns to end up in the CSV file, you can delete them here. Finally, click <strong>Save </strong>job and edit script.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-36.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-36.png" alt="" width="1175" height="488" class="aligncenter size-full wp-image-8676" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-36.png 1175w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-36-300x125.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-36-1024x425.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-36-768x319.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-36-1170x486.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-36-585x243.png 585w" sizes="(max-width: 1175px) 100vw, 1175px" /></a><br />
You&#8217;ll be presented with the Python script. The problem with this script is that for each record in DynamoDB, it will generate a separate file. If you are fine with this approach, just click <strong>Run job</strong> and see the logs. This isn&#8217;t a behavior that I expected, so I <a href="https://stackoverflow.com/questions/55515251/aws-glue-job-how-to-merge-multiple-output-csv-files-in-s3" rel="noopener noreferrer" target="_blank">found </a>a way to merge all these single files into a single file. If you have millions of records, this approach is not recommended. Anyway, these are the modifications that I made so I get a single file.<br />
<strong>Original script.</strong></p>
<pre class="brush: python; title: ; notranslate">
...
applymapping1 = ApplyMapping.apply(frame = datasource0, mappings = &#x5B;(&quot;sesreportingmta&quot;, &quot;string&quot;, &quot;sesreportingmta&quot;, &quot;string&quot;), (&quot;sesmessagetype&quot;, &quot;string&quot;, &quot;sesmessagetype&quot;, &quot;string&quot;), (&quot;sessourceaddress&quot;, &quot;string&quot;, &quot;sessourceaddress&quot;, &quot;string&quot;), (&quot;sesmessageid&quot;, &quot;string&quot;, &quot;sesmessageid&quot;, &quot;string&quot;), (&quot;sesdestinationaddress&quot;, &quot;string&quot;, &quot;sesdestinationaddress&quot;, &quot;string&quot;), (&quot;sesbouncesummary&quot;, &quot;string&quot;, &quot;sesbouncesummary&quot;, &quot;string&quot;), (&quot;snspublishtime&quot;, &quot;string&quot;, &quot;snspublishtime&quot;, &quot;string&quot;), (&quot;ttl&quot;, &quot;long&quot;, &quot;ttl&quot;, &quot;long&quot;)], transformation_ctx = &quot;applymapping1&quot;)
## @type: DataSink
## @args: &#x5B;connection_type = &quot;s3&quot;, connection_options = {&quot;path&quot;: &quot;s3://aws-glue-seslogs&quot;}, format = &quot;csv&quot;, transformation_ctx = &quot;datasink2&quot;]
## @return: datasink2
## @inputs: &#x5B;frame = applymapping1]
datasink2 = glueContext.write_dynamic_frame.from_options(frame = applymapping1, connection_type = &quot;s3&quot;, connection_options = {&quot;path&quot;: &quot;s3://aws-glue-seslogs&quot;}, format = &quot;csv&quot;, transformation_ctx = &quot;datasink2&quot;)
job.commit()
</pre>
<p><strong>Modified script.</strong></p>
<pre class="brush: python; highlight: [2,7]; title: ; notranslate">
applymapping1 = ApplyMapping.apply(frame = datasource0, mappings = &#x5B;(&quot;sesreportingmta&quot;, &quot;string&quot;, &quot;sesreportingmta&quot;, &quot;string&quot;), (&quot;sesmessagetype&quot;, &quot;string&quot;, &quot;sesmessagetype&quot;, &quot;string&quot;), (&quot;sessourceaddress&quot;, &quot;string&quot;, &quot;sessourceaddress&quot;, &quot;string&quot;), (&quot;sesmessageid&quot;, &quot;string&quot;, &quot;sesmessageid&quot;, &quot;string&quot;), (&quot;sesdestinationaddress&quot;, &quot;string&quot;, &quot;sesdestinationaddress&quot;, &quot;string&quot;), (&quot;sesbouncesummary&quot;, &quot;string&quot;, &quot;sesbouncesummary&quot;, &quot;string&quot;), (&quot;snspublishtime&quot;, &quot;string&quot;, &quot;snspublishtime&quot;, &quot;string&quot;), (&quot;ttl&quot;, &quot;long&quot;, &quot;ttl&quot;, &quot;long&quot;)], transformation_ctx = &quot;applymapping1&quot;)
repartition = applymapping1.repartition(1)
## @type: DataSink
## @args: &#x5B;connection_type = &quot;s3&quot;, connection_options = {&quot;path&quot;: &quot;s3://aws-glue-seslogs&quot;}, format = &quot;csv&quot;, transformation_ctx = &quot;datasink2&quot;]
## @return: datasink2
## @inputs: &#x5B;frame = applymapping1]
datasink2 = glueContext.write_dynamic_frame.from_options(frame = repartition, connection_type = &quot;s3&quot;, connection_options = {&quot;path&quot;: &quot;s3://aws-glue-seslogs&quot;}, format = &quot;csv&quot;, transformation_ctx = &quot;datasink2&quot;)
job.commit()
</pre>
<p>Pretty much, I&#8217;ve added one line #2 <strong>repartition = &#8230;</strong> after <strong>applymapping1 = &#8230;</strong> and then replaced <strong>applymapping1 </strong>in line #7 with <strong>repartition </strong>in the <strong>datasink2 = &#8230;</strong> line. Now, if you run the job and wait to finish, you&#8217;ll see a single file under your S3 bucket.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-37.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-37.png" alt="" width="872" height="371" class="aligncenter size-full wp-image-8677" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-37.png 872w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-37-300x128.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-37-768x327.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-37-585x249.png 585w" sizes="(max-width: 872px) 100vw, 872px" /></a><br />
&#8230; and the content of the file.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/08/P135-38.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/08/P135-38.png" alt="" width="780" height="111" class="aligncenter size-full wp-image-8678" srcset="https://blog.andreev.it/wp-content/uploads/2019/08/P135-38.png 780w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-38-300x43.png 300w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-38-768x109.png 768w, https://blog.andreev.it/wp-content/uploads/2019/08/P135-38-585x83.png 585w" sizes="(max-width: 780px) 100vw, 780px" /></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/08/aws-get-ses-bounce-complaint-delivery-details-using-lambda-sns-and-dynamodb-or-aws-glue/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
