<?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: Markus Toivakka</title>
    <description>The latest articles on DEV Community by Markus Toivakka (@markymarkus).</description>
    <link>https://dev.to/markymarkus</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%2F694827%2Fe9f23704-4280-45f1-ba45-9bc1a02961c7.png</url>
      <title>DEV Community: Markus Toivakka</title>
      <link>https://dev.to/markymarkus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/markymarkus"/>
    <language>en</language>
    <item>
      <title>My notes on AWS IPAM</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Fri, 19 Dec 2025 05:15:08 +0000</pubDate>
      <link>https://dev.to/aws-builders/my-notes-on-aws-ipam-4m7a</link>
      <guid>https://dev.to/aws-builders/my-notes-on-aws-ipam-4m7a</guid>
      <description>&lt;p&gt;AWS IPAM helps you avoid IP address conflicts by providing a single source of truth for CIDR allocation. It also offers features such as monitoring usage and compliance across accounts and regions—but those are out of scope here.&lt;/p&gt;

&lt;p&gt;In this post, I focus on a very basic (and very common) use case: using AWS IPAM to allocate a &lt;strong&gt;non‑overlapping CIDR&lt;/strong&gt; for a newly created VPC. How hard can that be? As it turns out, not very—but there are some practical gotchas worth knowing about. This post is a collection of those lessons learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  What works well
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Seamless VPC integration
&lt;/h3&gt;

&lt;p&gt;Once AWS IPAM is configured, CIDR allocation for VPCs is refreshingly simple. To appreciate this, consider some &lt;em&gt;non‑seamless&lt;/em&gt; alternatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CloudFormation custom resource that triggers on VPC creation and pulls the next free CIDR from an on‑prem IPAM&lt;/li&gt;
&lt;li&gt;A mix of bash scripting in CI/CD pipelines and IaC templates&lt;/li&gt;
&lt;li&gt;Manual Excel bookkeeping combined with click‑ops (😱)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With AWS IPAM, none of that is needed. VPC creation can look as simple as this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ipv4_ipam_pool_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-ipam-pool-id"&lt;/span&gt;
  &lt;span class="nx"&gt;ipv4_netmask_length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS IPAM automatically picks the next available &lt;code&gt;/21&lt;/code&gt; from the pool. When the VPC is deleted, the CIDR is returned to the pool (typically within ~10 minutes) and becomes available for reuse. If the account holding the VPC is deleted, VPC allocation stays in the pool until 90-days post-closure period has passed. &lt;/p&gt;




&lt;h3&gt;
  
  
  IP pools
&lt;/h3&gt;

&lt;p&gt;IP pools are a core IPAM concept and work largely as expected. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build a hierarchical structure (e.g. &lt;code&gt;company → department → region → environment&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Use a flat structure(e.g. &lt;code&gt;all-eu-west-1-cidrs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Combine both approaches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each pool contains one or more provisioned CIDR blocks. When a pool runs out of free address space, you can add additional CIDRs, and AWS IPAM will automatically start using them for future allocations.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sharing pools across accounts
&lt;/h3&gt;

&lt;p&gt;IPAM pools can be shared with other AWS accounts using AWS RAM. If your AWS Organization is structured with OUs (as it should be), this works nicely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share production pools with production OUs&lt;/li&gt;
&lt;li&gt;Share non‑prod pools with non‑prod OUs&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Accounts that receive a shared pool can see &lt;em&gt;all&lt;/em&gt; allocations within that pool, including VPC ID, CIDR, and Account ID.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Importing existing VPC CIDRs
&lt;/h3&gt;

&lt;p&gt;If you already have VPCs with manually assigned CIDRs—or CIDRs allocated via other automation—AWS IPAM can import those existing VPCs into pools.&lt;/p&gt;

&lt;p&gt;Think of this as &lt;strong&gt;syncing your current state&lt;/strong&gt; into AWS IPAM. From that point on, you can confidently allocate CIDRs for new VPCs, knowing that overlaps are prevented.&lt;/p&gt;




&lt;h2&gt;
  
  
  What doesn’t work so well
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;p&gt;AWS IPAM pricing is based on &lt;strong&gt;managed IP addresses per hour&lt;/strong&gt;. When integrated with AWS Organizations, you are billed for every IP address in use.&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; mark CIDR blocks in IPAM as ignored, in which case AWS skips managing those ranges—but any IP addresses actively in use are still billed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Missing features
&lt;/h2&gt;

&lt;p&gt;The core functionality works well, but several advanced (yet very reasonable) features are missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configurable de‑allocation grace periods:&lt;/strong&gt;&lt;br&gt;
When a VPC is deleted, its CIDR is released back to the pool after ~10 minutes. In some environments, you might want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A multi‑day grace period (e.g. 10 days)&lt;/li&gt;
&lt;li&gt;Manual approval before reuse&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;More useful pool‑level metrics:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Pool utilization is reported only as a percentage. Metrics such as &lt;em&gt;"IP addresses remaining"&lt;/em&gt; would make it much easier to build meaningful alarms across pools of different sizes.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Events for allocations and de‑allocations:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
VPC CIDR allocation and release events would be extremely useful for automation and auditing purposes. Those would make it easier also to implement custom workarounds for missing features like grace period. Now only way is to periodically poll IPAM pools for changes. Polling will be discussed further on the next section. &lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Weighted CIDR blocks at pool level:&lt;/strong&gt; &lt;br&gt;&lt;br&gt;
In most cases, you are having multiple CIDRs on the pool. It would be beneficial to have control which one is used first for VPC allocations. This would enable more efficient CIDR usage.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Things to be aware of
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Regions and locale
&lt;/h3&gt;

&lt;p&gt;AWS IPAM itself is created in a &lt;strong&gt;single region&lt;/strong&gt;, but it can manage pools across multiple regions.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IPAM is created in &lt;code&gt;eu-west-1&lt;/code&gt; home region.&lt;/li&gt;
&lt;li&gt;Pools exist in &lt;code&gt;eu-west-1&lt;/code&gt; and &lt;code&gt;eu-north-1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In IPAM terminology, each pool has a &lt;strong&gt;locale&lt;/strong&gt; (its region).&lt;/p&gt;

&lt;h4&gt;
  
  
  UI vs API behavior
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AWS Console:&lt;/strong&gt;&lt;br&gt;
All pools and allocations are visible in the region where IPAM is created, regardless of pool locale. Same behaviour continues on share recipient accounts. All pool shares are visible on RAM in the home region but the pool can be used only on the region specified by pool &lt;code&gt;locale&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AWS CLI / SDK:&lt;/strong&gt;&lt;br&gt;
This is where things get tricky. You must query pool allocations from the &lt;em&gt;same region as the pool’s locale&lt;/em&gt;. Pool CIDRs then again must be queried from the &lt;em&gt;same region where AWS IPAM is deployed&lt;/em&gt;. The result is sligthly awkward control flow. You end up juggling multiple boto3 clients, each valid only for a specific subset of API calls.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example using boto3:&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;ec2&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;ec2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu-west-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Works. Pool is on the same region as AWS IPAM
&lt;/span&gt;&lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ipam_pool_cidrs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IpamPoolId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EU-WEST-1-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ipam_pool_allocations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IpamPoolId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EU-WEST-1-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Does NOT work. 
&lt;/span&gt;&lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ipam_pool_allocations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IpamPoolId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EU-NORTH-1-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Correct approach. Get boto3 client on the same region as pool locale.
&lt;/span&gt;&lt;span class="n"&gt;ec2_eun1&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;ec2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu-north-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ec2_eun1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ipam_pool_allocations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IpamPoolId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EU-NORTH-1-ID&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;h3&gt;
  
  
  Resource discovery quirks
&lt;/h3&gt;

&lt;p&gt;AWS IPAM &lt;a href="https://docs.aws.amazon.com/vpc/latest/ipam/res-disc-work-with.html" rel="noopener noreferrer"&gt;Resource Discovery&lt;/a&gt; keeps an inventory of IP address usage across accounts, but its behavior is not always consistent.&lt;br&gt;
Overall there is a strict separation of duties between IPAM Pools and IPAM Resource Discovery. For example, you can't use Resource Discovery to query the IPAM pool from which a VPC was allocated. Conversely, if you need to access VPC tags, those are exposed only via Resource Discovery and are not available at the IPAM pool level.&lt;/p&gt;

&lt;p&gt;Because of these inconsistencies and syncing issues, getting VPC configuration which contains pool level data requires active polling from both IPAM pool and Resource Discovery.&lt;/p&gt;

&lt;p&gt;For common VPC operations IPAM behaves as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When a VPC is created using an IPAM pool, the CIDR allocation is immediately visible in the pool, but it may take several minutes before the corresponding VPC shows up in &lt;strong&gt;Resource Discovery&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When an AWS account is closed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPCs disappear from Resource Discovery almost immediately&lt;/li&gt;
&lt;li&gt;CIDR allocations remain associated with the pool&lt;/li&gt;
&lt;li&gt;After the ~90 days post-closure period after the account is permanently deleted, the CIDRs are released back to the pool&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AWS IPAM solves the core problem it’s meant to solve: allocating non-overlapping CIDRs for VPCs without custom automation or human bookkeeping. However, there is still a somewhat unfinished feel, with missing events for automation and region/locale quirks that are easy to trip over. This pushes you to build workarounds for missing or incomplete features—something that ideally shouldn’t be necessary for a managed AWS service.&lt;/p&gt;

&lt;p&gt;Pricing can also become noticeable in large environments. Billing is based on managed IP addresses per hour, so organizations with many accounts and large CIDR ranges should do the math upfront.&lt;/p&gt;

&lt;p&gt;And hey, since it’s almost Christmas, here’s my wish: ditch AWS IPAM Resource Discovery altogether. Keep monitoring at the VPC / subnet level, no need to track ENI-level details. Attach VPC metadata (like tags) directly to pool allocations. And emit pool lifecycle and allocation events to Eventbridge. Keep it simple. And please, make it free — or at least charge per VPC.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Using AWS Identity Center (SSO) tokens to script across multiple accounts</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Sat, 11 Oct 2025 12:30:50 +0000</pubDate>
      <link>https://dev.to/aws-builders/using-aws-identity-center-sso-tokens-to-script-across-multiple-accounts-352l</link>
      <guid>https://dev.to/aws-builders/using-aws-identity-center-sso-tokens-to-script-across-multiple-accounts-352l</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; AWS Identity Center (formerly AWS SSO) stores a short-lived &lt;code&gt;accessToken&lt;/code&gt; in a local JSON cache after you run &lt;code&gt;aws sso login&lt;/code&gt;. You can exchange that token for per-account IAM temporary credentials using the &lt;code&gt;sso.get_role_credentials&lt;/code&gt; API and then use those credentials with &lt;code&gt;boto3.Session&lt;/code&gt; to run operations across multiple accounts. This post explains how the token flow works, security implications (yes — plain text cache), and gives a hands-on Python example you can adapt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;How Identity Center tokens work (high level)&lt;/li&gt;
&lt;li&gt;Token → IAM credentials: what happens under the hood&lt;/li&gt;
&lt;li&gt;Security considerations&lt;/li&gt;
&lt;li&gt;What an attacker can do with a stolen accessToken&lt;/li&gt;
&lt;li&gt;Full example: running a ReadOnly check in all your SSO accounts using &lt;code&gt;AWSReadOnlyAccess&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hardening recommendations and alternatives&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1) How Identity Center tokens work (high level)
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;aws sso login&lt;/code&gt; the CLI performs an OIDC/OAuth flow and writes a small JSON token file to the SSO cache directory (usually &lt;code&gt;~/.aws/sso/cache/&lt;/code&gt;). The JSON contains an &lt;code&gt;accessToken&lt;/code&gt; (a Bearer token) and an &lt;code&gt;expiresAt&lt;/code&gt; timestamp.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;accessToken&lt;/code&gt; represents your authenticated Identity Center session (your user + any required MFA). It is not long-lived — the token has an expiry (1-12h, depending on your org's config).&lt;/p&gt;

&lt;p&gt;You cannot directly use that &lt;code&gt;accessToken&lt;/code&gt; like AWS credentials; instead you &lt;em&gt;exchange&lt;/em&gt; it for IAM credentials for a specific account and role via the Identity Center &lt;code&gt;sso&lt;/code&gt; API.&lt;/p&gt;

&lt;h2&gt;
  
  
  2) Token → IAM credentials: what happens under the hood
&lt;/h2&gt;

&lt;p&gt;The key API is &lt;code&gt;sso.get_role_credentials&lt;/code&gt; (available in &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sso/client/get_role_credentials.html" rel="noopener noreferrer"&gt;boto3&lt;/a&gt;). You call it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;accountId&lt;/code&gt; — the target AWS account you want access to&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;roleName&lt;/code&gt; — the Identity Center role name assigned in that account (for example, &lt;code&gt;AWSReadOnlyAccess&lt;/code&gt;). You can also get available roles from &lt;code&gt;sso.list_account_roles&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;accessToken&lt;/code&gt; — the cached token from &lt;code&gt;~/.aws/sso/cache/*.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;get_role_credentials&lt;/code&gt; call returns a short-lived set of credentials: &lt;code&gt;accessKeyId&lt;/code&gt;, &lt;code&gt;secretAccessKey&lt;/code&gt;, and &lt;code&gt;sessionToken&lt;/code&gt;. These are standard STS-style credentials and you use them to create a &lt;code&gt;boto3.Session&lt;/code&gt; that will act in the target account with the permissions associated to the role.&lt;/p&gt;

&lt;h2&gt;
  
  
  3) Security considerations
&lt;/h2&gt;

&lt;p&gt;Important: the JSON files under &lt;code&gt;~/.aws/sso/cache/&lt;/code&gt; are &lt;strong&gt;plain text&lt;/strong&gt; JSON. The &lt;code&gt;accessToken&lt;/code&gt; inside them is effectively a bearer token that can be exchanged for AWS credentials until it expires.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat the &lt;code&gt;accessToken&lt;/code&gt; like a password / secret bearer token.&lt;/li&gt;
&lt;li&gt;Protect the &lt;code&gt;~/.aws/sso/cache&lt;/code&gt; directory with strict file permissions (e.g., &lt;code&gt;chmod 600&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Monitor and rotate your sessions: use the shortest practical session duration configured in your Identity Center settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4) What an attacker can do with a stolen accessToken
&lt;/h2&gt;

&lt;p&gt;If a bad actor gains access to your SSO &lt;code&gt;accessToken&lt;/code&gt; file, they can effectively impersonate you in AWS until that token expires. Here’s what happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The attacker can use your &lt;code&gt;accessToken&lt;/code&gt; on &lt;strong&gt;their own computer&lt;/strong&gt; to call AWS SSO APIs (&lt;code&gt;ListAccounts&lt;/code&gt;, &lt;code&gt;ListAccountRoles&lt;/code&gt;, and &lt;code&gt;GetRoleCredentials&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;GetRoleCredentials&lt;/code&gt;, they can obtain temporary IAM credentials for &lt;strong&gt;every account and role&lt;/strong&gt; you have access to through Identity Center.&lt;/li&gt;
&lt;li&gt;Those temporary credentials give them the same level of access that your assigned roles do — including read or write permissions, depending on your configuration.&lt;/li&gt;
&lt;li&gt;Once they have those IAM keys, they can use standard AWS APIs (e.g., S3, EC2, IAM) from anywhere, even outside your corporate network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why the SSO token must be treated like a highly sensitive credential — it’s effectively a universal key for your Identity Center session.&lt;/p&gt;

&lt;p&gt;Even though the keys are short-lived, this can still result in data exfiltration, resource modification, or privilege escalation depending on your roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  5) Full example: running a ReadOnly check in all your SSO accounts using &lt;code&gt;AWSReadOnlyAccess&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This example hardcodes the role name &lt;code&gt;AWSReadOnlyAccess&lt;/code&gt;. It lists account IDs via SSO, then requests role credentials for &lt;code&gt;AWSReadOnlyAccess&lt;/code&gt; in each account and performs an &lt;code&gt;sts.get_caller_identity()&lt;/code&gt; check.&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="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run a ReadOnly boto3 action in all AWS SSO accounts using a fixed role name&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&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;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;

&lt;span class="n"&gt;AWS_SSO_CACHE_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/.aws/sso/cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;FIXED_ROLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWSReadOnlyAccess&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# fixed role name
&lt;/span&gt;
&lt;span class="n"&gt;SSO_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;sso&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_latest_sso_token&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&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;AWS_SSO_CACHE_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No SSO cache JSON files found in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AWS_SSO_CACHE_DIR&lt;/span&gt;&lt;span class="si"&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;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getmtime&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="n"&gt;latest&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;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&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;accessToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&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;expiresAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;exp_dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Z&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;+00:00&lt;/span&gt;&lt;span class="sh"&gt;'&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;expires_at&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exp_dt&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_role_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SSO_CLIENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_role_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;roleName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;role_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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;roleCredentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No roleCredentials for account &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accessKeyId&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;aws_secret_access_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secretAccessKey&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;aws_session_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessionToken&lt;/span&gt;&lt;span class="sh"&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;def&lt;/span&gt; &lt;span class="nf"&gt;list_sso_account_ids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;account_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;paginator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SSO_CLIENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_paginator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list_accounts&lt;/span&gt;&lt;span class="sh"&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;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paginator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;access_token&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;acct&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&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;accountList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
            &lt;span class="n"&gt;account_ids&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;acct&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;account_ids&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_latest_sso_token&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;exp&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SSO token expired — run `aws sso login` again.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;account_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list_sso_account_ids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;account_ids&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No accounts discovered for this SSO session.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;acct_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;account_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_role_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acct_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FIXED_ROLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sess&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="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu-west-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sess&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;sts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;who&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_caller_identity&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;--&amp;gt; Account &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;acct_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, caller identity:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;who&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Arn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  failed to get role credentials or call STS:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Could indicate the role isn't assigned to you in that account
&lt;/span&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;  runtime error:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;AWSReadOnlyAccess&lt;/code&gt; is not assigned to your Identity Center principal in a particular account, &lt;code&gt;get_role_credentials&lt;/code&gt; will fail for that account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6) Hardening recommendations and alternatives
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;File permissions&lt;/strong&gt;: make sure &lt;code&gt;~/.aws/sso/cache&lt;/code&gt; is only readable by your user (&lt;code&gt;chmod 700 ~/.aws/sso/cache &amp;amp;&amp;amp; chmod 600 ~/.aws/sso/cache/*.json&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid long sessions&lt;/strong&gt;: Keep AWS Identity Center &lt;a href="https://docs.aws.amazon.com/singlesignon/latest/userguide/howtosessionduration.html" rel="noopener noreferrer"&gt;session duration&lt;/a&gt; as small as practical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit &amp;amp; monitor&lt;/strong&gt;: enable CloudTrail and monitor &lt;code&gt;GetRoleCredentials&lt;/code&gt;/&lt;code&gt;AssumeRole&lt;/code&gt; patterns for suspicious usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least privilege&lt;/strong&gt;: maybe you don’t need that &lt;code&gt;AWSAdministratorAccess&lt;/code&gt; role in every account — start with &lt;code&gt;AWSReadOnlyAccess&lt;/code&gt; and escalate only when necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use credential helpers&lt;/strong&gt;: Consider using third-party tools for AWS credential handling, such as &lt;a href="https://github.com/fwdcloudsec/granted" rel="noopener noreferrer"&gt;granted&lt;/a&gt;
or &lt;a href="https://github.com/99designs/aws-vault" rel="noopener noreferrer"&gt;aws-vault&lt;/a&gt;
, to isolate and securely manage session tokens.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>python</category>
    </item>
    <item>
      <title>Terraform AWS multi-region deployments: region meta-argument in Beta</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Sat, 07 Jun 2025 12:48:07 +0000</pubDate>
      <link>https://dev.to/aws-builders/terraform-aws-multi-region-deployments-region-meta-argument-in-beta-o76</link>
      <guid>https://dev.to/aws-builders/terraform-aws-multi-region-deployments-region-meta-argument-in-beta-o76</guid>
      <description>&lt;p&gt;Terraform holds a solid position in the ADOPT category of &lt;a href="https://medium.com/s-group-dev/tech-radar-update-e2a56f184183" rel="noopener noreferrer"&gt;SOK Tech Radar&lt;/a&gt;, and for a good reason. Most our teams rely on it to define and provision cloud infrastructure across AWS, Azure, and other platforms.&lt;/p&gt;

&lt;p&gt;One of the Terraform’s biggest strengths lies in its modular architecture, which allows teams to use and share best practices through reusable modules.&lt;/p&gt;

&lt;p&gt;Another reason to love Terraform? The frequent updates to HCL that often make me go “wooh”! This post dives into one such update — still in beta but offering a much cleaner approach to multi-region deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Traditional Approach: Provider Aliases
&lt;/h3&gt;

&lt;p&gt;Managing AWS resources across multiple regions has traditionally required separate provider configurations for each of the regions. If you’ve ever needed to deploy something like an SNS topic in multiple regions, you probably know the spiel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider "aws" {
  alias  = "eu_north_1"
  region = "eu-north-1"
}

provider "aws" {
  alias  = "eu_west_1"
  region = "eu-west-1"
}

resource "aws_sns_topic" "test_eu_north_1" {
  provider = aws.eu_north_1
}

resource "aws_sns_topic" "test_eu_west_1" {
  provider = aws.eu_west_1
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach works, but it’s verbose and repetitive. The real limitation becomes apparent when you try to scale: the provider argument couldn't be used dynamically withfor_each, making regional deployments an exercise in copy-pasting and configuration clutter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter the Region Meta-Argument
&lt;/h3&gt;

&lt;p&gt;The Terraform AWS Provider 6.0.0 (currently in beta) introduces a new region meta-argument that overrides the provider-level region configuration. This allows you to specify the region directly at the resource level, eliminating the need for multiple provider aliases.&lt;/p&gt;

&lt;p&gt;More importantly, the region meta-argument enables dynamic for_each loops for regional deployments, transforming how we handle multi-region infrastructure.&lt;/p&gt;

&lt;p&gt;Here’s how you can now deploy the same SNS topics across multiple regions with significantly less boilerplate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 6.0.0-beta2" # Ensure we are using the latest beta version
    }
  }
}

provider "aws" {}

locals {
  regions = ["eu-west-1", "eu-north-1"]
}

resource "aws_sns_topic" "test" {
   for_each = toset(local.regions)
   region = each.key
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is cleaner and easier to maintain. No more juggling multiple provider aliases or duplicating resource blocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;While the AWS Provider 6.0.0 is still in beta, this feature represents a significant step forward in simplifying multi-region deployments. As teams increasingly adopt multi-region architectures for resilience and compliance, having cleaner, more maintainable Terraform configurations becomes crucial. This feature is definitely worth experimenting with!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Building Bedrock Agents for AWS Account Metadata and Cost Analysis</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Sat, 04 Jan 2025 07:01:12 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-bedrock-agents-for-aws-account-metadata-and-cost-analysis-28c0</link>
      <guid>https://dev.to/aws-builders/building-bedrock-agents-for-aws-account-metadata-and-cost-analysis-28c0</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Small language models (SLMs) often take a back seat to their larger GenAI counterparts, but I’ve been pondering how they could be used in practical, everyday scenarios like AWS account management. While it’s true that GenAI is the industry’s hype word, applying these tools to  familiar problems can offer an excellent framework for learning and innovation. That’s exactly what I aim to focus on in this post.&lt;/p&gt;

&lt;p&gt;The goal? To enable the Bedrock Agent to handle queries like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Get July's costs for Bill's AWS accounts." or&lt;br&gt;
"List all prod - accounts"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here’s how it works: The agent will first fetch all AWS accounts where Bill is tagged as the owner using the Organizations API. It will then use the Cost Explorer API to perform cost analysis for each of those accounts. By orchestrating these steps, the Bedrock Agent acts as a practical assistant for AWS account management tasks.&lt;/p&gt;

&lt;p&gt;In the next session, We’ll take a deeper dive into both of these AWS APIs to understand how they work together to achieve our goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Organizations API
&lt;/h2&gt;

&lt;p&gt;AWS Organizations Tags offer a powerful way to structure and manage AWS accounts by attaching metadata in the form of tags. These tags can be used to categorize accounts and answer queries like, “Which department owns this account?” or “What environment does this account belong to?”&lt;/p&gt;

&lt;p&gt;By leveraging tags such as &lt;code&gt;Owner&lt;/code&gt; and &lt;code&gt;Environment&lt;/code&gt;, you can create a simple framework for managing accounts. In this POC, these tags play a central role in enabling the Bedrock Agent to fetch additional information about AWS accounts. That metadata is then translated into AWS account IDs, which is essential for the next step: cost analysis. For more details, see the &lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_tagging.html" rel="noopener noreferrer"&gt;AWS Organizations Tagging&lt;/a&gt; documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Explorer API
&lt;/h2&gt;

&lt;p&gt;For cost analysis, we’re keeping things simple. The Bedrock Agent queries the &lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/ce-api.html" rel="noopener noreferrer"&gt;Cost Explorer API&lt;/a&gt; for account costs over a specified period. While this method is far from comprehensive—anyone familiar with AWS cost management knows there’s much more nuance—it serves as a solid starting point.&lt;/p&gt;

&lt;p&gt;One particularly cool aspect of using a language model interface is its ability to interpret natural language into precise API parameters. For example, the Cost Explorer API expects a "Start date for filtering costs in YYYY-MM-DD format." When a query like "last month's costs" is submitted, the model intelligently converts "last month" into the correct date range and passes it as a parameter to the API. This highlights the value of combining language models with AWS APIs to streamline complex workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  About Foundation Models
&lt;/h2&gt;

&lt;p&gt;The first version of this solution came to life in the fall of 2024. Initially, I used Claude 3.5 Haiku and Sonnet models. While they delivered excellent performance, it felt like overkill for the small, straightforward prompts in this project. So, when Amazon introduced &lt;a href="https://docs.aws.amazon.com/nova/latest/userguide/what-is-nova.html" rel="noopener noreferrer"&gt;the Nova models&lt;/a&gt; at re:Invent 2024, I jumped at the chance to see how well they’d perform for this use case. For this demo, I opted to use the Amazon Nova Lite model, which proved to be a solid fit.&lt;/p&gt;

&lt;p&gt;However, Nova Lite's smaller size did reveal a few limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Accuracy Issues: When validating cost analysis results (e.g., "All dev accounts' costs in November") using a basic calculator, the numbers often didn’t match. Even for simpler queries like "Get account count by environment," the model sometimes returned totals that exceeded the actual number of accounts in the mock dataset.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Token Limitations: Nova Lite has a maximum output of 5,000 tokens. For queries involving tens of accounts with metadata, the responses often exceeded this limit, causing the output to stop mid-sentence. While you can prompt the agent to continue with commands like "go on," this disrupts the workflow.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That said, Nova Lite's speed is a standout feature. For short prompts, the quick response time makes for a snappy and efficient experience. It also integrates seamlessly with Bedrock Agents and Lambda functions, making it an excellent choice for building lightweight solutions.&lt;/p&gt;

&lt;p&gt;For organizations with hundreds of accounts or more complex queries, however, using a larger model would make more sense, as the data volume and accuracy demands increase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of the Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;This workflow diagram demonstrates how a user's query is processed and translated into API calls.&lt;br&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%2Frinbjnzvr35npf36gr6y.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%2Frinbjnzvr35npf36gr6y.png" alt="Image description" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;invoke_agent.py&lt;/code&gt; cli
&lt;/h3&gt;

&lt;p&gt;To interact with the Bedrock Agent, I created a simple Python script called &lt;code&gt;invoke_agent.py&lt;/code&gt;. This script serves as a command-line interface, making it easy to submit queries to the agent. Additionally, it prints the input and output token counts for each query, offering insights into the efficiency and resource usage of the interactions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation Walkthrough
&lt;/h2&gt;

&lt;p&gt;If you’d like to follow along, all source code is available in this GitHub repository: &lt;a href="https://github.com/markymarkus/bedrock-agent-accounts" rel="noopener noreferrer"&gt;https://github.com/markymarkus/bedrock-agent-accounts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To use this solution, you’ll also need access to the &lt;code&gt;amazon.nova-lite-v1:0&lt;/code&gt; Bedrock foundation model. Currently, Amazon Nova models are only available in the &lt;code&gt;us-east-1&lt;/code&gt; region, so be sure to deploy your resources there.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using Mock vs. Real Data
&lt;/h3&gt;

&lt;p&gt;In my private life, I only manage a handful of accounts—and I’d rather not share their details publicly. To address this, I developed functions to generate mock account and billing data. For this POC, I worked with a simulated AWS Organization containing 30 accounts, each enriched with mock metadata and billing information to provide a realistic testing environment.&lt;/p&gt;

&lt;p&gt;By default, this solution uses mock data for account and cost queries. If you’d like to experiment with real account and cost data, you can disable the mock data by deploying the CloudFormation template with the parameter: &lt;code&gt;EnableMockData: false&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Deployment
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 mb s3://temp-bedrock-cf-staging

aws cloudformation package --s3-bucket temp-bedrock-cf-staging --output-template-file packaged.yaml --region us-east-1 --template-file template.yaml

aws cloudformation deploy --stack-name dev-bedrock-accounts-agent --template-file packaged.yaml --region us-east-1 --capabilities CAPABILITY_IAM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;At this point the Bedrock account agent is ready. Next update created agent's ID to &lt;code&gt;invoke_agent.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws cloudformation describe-stacks --stack-name dev-bedrock-accounts-agent --query 'Stacks[0].Outputs[?OutputKey==`AgentId`].OutputValue'

And then copy-paste the ID to `invoke_agent.py` agent_id variable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ...And ACTION!
&lt;/h2&gt;

&lt;p&gt;Here we have finally recording of running queries to Bedrock Agent. &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%2Fyr03mh6i990w6715n6t7.gif" 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%2Fyr03mh6i990w6715n6t7.gif" alt="GIF Animation" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yr03mh6i990w6715n6t7.gif" rel="noopener noreferrer"&gt;DIRECT LINK to the image, in case dev.to doesn't allow animated GIF&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;p&gt;Throughout this project, several important points emerged. While seasoned AI professionals might find some of these familiar, they proved invaluable for my work and could benefit others as well:&lt;/p&gt;

&lt;h3&gt;
  
  
  LLMs Don’t Know the Current Date
&lt;/h3&gt;

&lt;p&gt;By default, Claude’s knowledge is limited to 2023, meaning prompts like "Get last month's costs" return results based on outdated information. To resolve this, I modified the invoke_agent.py script to append the current date to &lt;code&gt;promptSessionAttributes&lt;/code&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Bedrock Agents Are Token-Hungry
&lt;/h3&gt;

&lt;p&gt;It’s impressive to observe how Foundation models reasons through API calls, orchestrating actions like retrieving account lists and fetching costs for each account. However, each step of this reasoning process requires the model to articulate its logic in the prompt, consuming a significant amount of tokens in the process. &lt;/p&gt;

&lt;h3&gt;
  
  
  Let the LLM Handle the Work
&lt;/h3&gt;

&lt;p&gt;In earlier iterations of the project, I required the agent to pass an exact email (e.g., "list all accounts where the owner is &lt;a href="mailto:markus_toivakka@myowndomain.fi"&gt;markus_toivakka@myowndomain.fi&lt;/a&gt;") to Lambda function. Lambda function then did filtering and returned corresponding account list. However, by shifting the filtering logic to the LLM, I enabled it to retrieve a full account list and filter accounts itself, allowing for more flexible queries like even "give all markus's accounts". &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This exercise gave me a clearer understanding of the key pieces of the puzzle: how AI Agents can automate tasks and how seamlessly an LLM interface can be added to an existing API. However, it also highlighted a crucial point—LLMs and AI Agents consume potentially a lot of data. There's an important trade-off to consider: Should filtering be handled in a Python function, or should the data be ingested and processed by the LLM itself?&lt;/p&gt;

&lt;p&gt;While getting the solution described in this blog to work was fairly straightforward, optimizing it for cost efficiency and grasping the broader dimensions of data processing in AI systems presents a much more complex challenge.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>rag</category>
      <category>finops</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Implement mTLS on AWS ALB with Self-Signed Certificates</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Tue, 17 Sep 2024 16:34:13 +0000</pubDate>
      <link>https://dev.to/aws-builders/implement-mtls-on-aws-alb-with-self-signed-certificates-9bf</link>
      <guid>https://dev.to/aws-builders/implement-mtls-on-aws-alb-with-self-signed-certificates-9bf</guid>
      <description>&lt;p&gt;In this post we'll walk through a step-by-step guide to implement mutual TLS (mTLS) configuration on AWS Application Load Balancer(ALB) and verifying the setup using &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;mTLS requires both the server and the client to have trusted certificates, issued by trusted CAs. If you are building mTLS on production use, I suggest to take a look at &lt;a href="https://docs.aws.amazon.com/privateca/latest/userguide/PcaWelcome.html" rel="noopener noreferrer"&gt;AWS Private CA&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;In this blog post, we'll be using self-signed certificates, which are certificates generated and signed locally without the involvement of a trusted Certificate Authority (CA). This allows us to create all necessary certificates directly on your local machine and upload them to AWS services. While self-signed certificates are typically not used in production due to their lack of trust from external entities, they offer a practical way to understand how mTLS works when a client initiates a session with a server.&lt;/p&gt;

&lt;p&gt;Let's dive in and get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate self-signed certificates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Generate x.509v3 Configuration Files
&lt;/h3&gt;

&lt;p&gt;When working with mTLS on ALB, it's important to use &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/mutual-authentication.html" rel="noopener noreferrer"&gt;x.509 Version 3&lt;/a&gt; certificates, as they are required for proper mutual authentication. OpenSSL certificates are by default Version 1 unless you explicitly specify otherwise. To ensure compatibility with ALB, we first need to create specific configuration files, which will be referenced during the certificate creation process.&lt;/p&gt;

&lt;p&gt;Below, we'll outline the steps to create these x.509v3 configuration files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# openssl-ca.cnf
[ req ]
default_bits        = 2048
default_md          = sha256
default_keyfile     = client-private.key
prompt              = no
distinguished_name  = req_distinguished_name
x509_extensions     = v3_ca

[ req_distinguished_name ]
C                   = FI
ST                  = State
L                   = Tampere
O                   = MyOrg
OU                  = IT
CN                  = MyCertificateAuthority

[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, keyCertSign, cRLSign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# openssl-client.cnf
[ v3_req ]
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Generate x.509v3 Certificates
&lt;/h3&gt;

&lt;p&gt;In this step, we will generate the necessary certificates for both the server (ALB with Lambda function) and the client (your laptop). These certificates will enable secure communication through mutual authentication.&lt;/p&gt;

&lt;h4&gt;
  
  
  Generate Certicates:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Generate the Private Key&lt;br&gt;
The private key is used to sign the certificates.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; client-private.key 4096
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create the CA Certificate&lt;br&gt;
The Certificate Authority (CA) certificate will be uploaded to AWS Load Balancer's Trust Store to establish trust.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 3650 &lt;span class="nt"&gt;-key&lt;/span&gt; client-private.key &lt;span class="nt"&gt;-out&lt;/span&gt; client-ca-cert.pem &lt;span class="nt"&gt;-config&lt;/span&gt; openssl-ca.cnf
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Generate Client Certificates&lt;br&gt;
Now, create two client certificates. These will be signed by the CA certificate created in the previous step.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate Certificate Signing Request (CSRs) for each client:
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; client-private.key &lt;span class="nt"&gt;-out&lt;/span&gt; client1.csr &lt;span class="nt"&gt;-subj&lt;/span&gt;  &lt;span class="s2"&gt;"/C=FI/ST=State/L=Tampere/O=MyOrg/OU=IT/CN=MyClient001"&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; client-private.key &lt;span class="nt"&gt;-out&lt;/span&gt; client2.csr &lt;span class="nt"&gt;-subj&lt;/span&gt; 
&lt;span class="s2"&gt;"/C=FI/ST=State/L=Tampere/O=MyOrg/OU=IT/CN=MyClient002"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Sign each CSR to create the certificates:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; client1.csr &lt;span class="nt"&gt;-CA&lt;/span&gt; client-ca-cert.pem &lt;span class="nt"&gt;-CAkey&lt;/span&gt; client-private.key &lt;span class="nt"&gt;-set_serial&lt;/span&gt; 01 &lt;span class="nt"&gt;-out&lt;/span&gt; client-public-1.pem &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-extensions&lt;/span&gt; v3_req &lt;span class="nt"&gt;-extfile&lt;/span&gt; openssl-client.cnf
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; client2.csr &lt;span class="nt"&gt;-CA&lt;/span&gt; client-ca-cert.pem &lt;span class="nt"&gt;-CAkey&lt;/span&gt; client-private.key &lt;span class="nt"&gt;-set_serial&lt;/span&gt; 01 &lt;span class="nt"&gt;-out&lt;/span&gt; client-public-2.pem &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-extensions&lt;/span&gt; v3_req &lt;span class="nt"&gt;-extfile&lt;/span&gt; openssl-client.cnf
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key Points
&lt;/h3&gt;

&lt;p&gt;At this stage, we have successfully generated three x509.v3 certificates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CA Certificate (&lt;code&gt;client-ca-cert.pem&lt;/code&gt;): This certificate will be uploaded to AWS ALB's Trust Store to establish trust between the ALB and clients.&lt;/li&gt;
&lt;li&gt;Client Certificates (&lt;code&gt;client-public-1.pem&lt;/code&gt; and &lt;code&gt;client-public-2.pem&lt;/code&gt;): These will be used by the clients (e.g., your laptop) to authenticate with the ALB during the mutual TLS handshake.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;
&lt;h2&gt;
  
  
  Create an Application Load Balancer configured for mTLS
&lt;/h2&gt;

&lt;p&gt;Before starting, ensure that your AWS CLI is properly configured. You'll also need a Cloudformation template to provision the required infrastructure.&lt;/p&gt;

&lt;p&gt;You can find a ready-made Cloudformation template and necessary resources here: &lt;a href="https://github.com/markymarkus/cloudformation/tree/master/alb_lambda_mtls" rel="noopener noreferrer"&gt;https://github.com/markymarkus/cloudformation/tree/master/alb_lambda_mtls&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Upload the CA Certificate to S3
&lt;/h3&gt;

&lt;p&gt;For mutual TLS (mTLS) authentication, ALB requires the CA certificate chain to be stored in an S3 bucket. This S3 bucket, along with the certificate object, will be referenced when the ALB's Trust Store is created. &lt;/p&gt;

&lt;p&gt;Run the following command to create a new S3 bucket(replace dev-trust-store-certs with your preferred bucket name). After that is done, CA Certificate is copied to the bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 mb s3://dev-trust-store-certs --region eu-west-1
aws s3 cp client-ca-cert.pem s3://dev-trust-store-certs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Provision the ALB infrastructure
&lt;/h3&gt;

&lt;p&gt;Now that the CA certificate is stored in S3, we can proceed to provision the Application Load Balancer and Lambda function using a CloudFormation template.&lt;/p&gt;

&lt;p&gt;Run the following command to deploy the ALB and backed Lambda function infrastructure. Make sure to update &lt;code&gt;parameters.json&lt;/code&gt; with the necessary configuration values(e.g., VPC, certificate and S3 bucket details).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws cloudformation deploy --stack-name mtls-demo --template-file alb_lambda_mtls.yaml --parameter-overrides file://parameters.json --capabilities CAPABILITY_IAM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt; &lt;br&gt;
 &lt;/p&gt;
&lt;h2&gt;
  
  
  Use cURL to Test mTLS
&lt;/h2&gt;

&lt;p&gt;The final step is to verify the mutual TLS (mTLS) handshake using cURL with the newly created ALB. We'll first attempt a standard TLS connection without client certificates, followed by a successful mTLS connection with the necessary certificates.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Test mTLS Connection Without Client Certificate
&lt;/h3&gt;

&lt;p&gt;Run the following cURL command to test a simple TLS connection to the ALB without providing a client certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl https://mtls-server.XXXXXXXX.fi 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will fail with the error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl: (35) Recv failure: Connection reset by peer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This failure occurs because the client did not present a certificate to prove its identity, which is required for mutual TLS authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Test mTLS with Client Certificates
&lt;/h3&gt;

&lt;p&gt;Now, let's add the client’s private key and public certificate to authenticate the client and complete the mTLS handshake&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl --key client-private.key --cert client-public-1.pem https://mtls-server.XXXXXXXX.fi 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, the command should succeed, and you should receive a response similar to the following to indicate that authorized client has invocated it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello: CN=MyClient001,OU=IT,O=MyOrg,L=Tampere,ST=State,C=FI  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response from the Lambda function confirms that the client has successfully authenticated with the ALB, indicating that mTLS is functioning as expected. This response includes details from the client’s certificate, verifying that the ALB has properly verified client's identity. &lt;br&gt;
To further test the setup, try updating the previous &lt;code&gt;curl&lt;/code&gt; command to use the &lt;code&gt;client-public-2.pem&lt;/code&gt; certificate. You'll notice that the response changes accordingly, reflecting the new client certificate being used for authentication.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we walked through the process of generating x.509v3 certificates, configuring an AWS Application Load Balancer for mutual TLS (mTLS), and successfully testing the setup using cURL. By securing communication between clients and the ALB with mTLS, you ensure that both parties authenticate each other, enhancing the security of your application.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>cli</category>
    </item>
    <item>
      <title>Multi-account AWS deployments with Terragrunt</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Thu, 28 Dec 2023 13:18:35 +0000</pubDate>
      <link>https://dev.to/aws-builders/multi-account-aws-deployments-with-terragrunt-4kod</link>
      <guid>https://dev.to/aws-builders/multi-account-aws-deployments-with-terragrunt-4kod</guid>
      <description>&lt;p&gt;Terragrunt is a thin wrapper around Terraform that provides extra layer to handle Terraform configurations. It makes it easier to manage .tf - remote states.&lt;/p&gt;

&lt;p&gt;In this blog post I'm focusing on using Terragrunt in the context of multi-account provisioning. Call it AWS account bootstrapping or landing zone, idea is the same: provision same identical resource to multiple AWS accounts and regions. I want to make it with least amount of copy-pasting and as dynamic as possible. &lt;/p&gt;

&lt;p&gt;I'm going to show how to use Terragrunt to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provision a resource to multiple AWS accounts.&lt;/li&gt;
&lt;li&gt;Manage all accounts' remote states in a single S3 - bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm going to use following account setup for the sample:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS &lt;em&gt;management account&lt;/em&gt; with AWS Organizations - enabled. I'm running &lt;code&gt;terragrunt&lt;/code&gt; using short time credentials from this account. Resources are provisioned to member accounts and Terraform states are stored to S3 and DynamoDB on this account.
&lt;/li&gt;
&lt;li&gt;Three &lt;em&gt;member accounts&lt;/em&gt;. Note: AWS Organizations automatically creates &lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_accounts_access.html" rel="noopener noreferrer"&gt;default role(&lt;code&gt;OrganizationAccountAccessRole&lt;/code&gt;)&lt;/a&gt; to every member account. Organizations default role is used to provision resources with Terraform. (Note: Default role used here is admin role. If you are considering to use this in production, make sure to use a role with scoped down access rights.)&lt;/li&gt;
&lt;/ul&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%2Fzt3p36llgvyop71wtm44.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%2Fzt3p36llgvyop71wtm44.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Walkthrough
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: Install &lt;a href="https://developer.hashicorp.com/terraform/install?product_intent=terraform" rel="noopener noreferrer"&gt;terraform&lt;/a&gt; and &lt;a href="https://terragrunt.gruntwork.io/docs/getting-started/install/" rel="noopener noreferrer"&gt;terragrunt&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;To get started with multi-account deployment, I'm using very minimal terragrunt structure. &lt;br&gt;
You can clone this project from: &lt;a href="https://github.com/markymarkus/terragrunt_aws_multi_account" rel="noopener noreferrer"&gt;https://github.com/markymarkus/terragrunt_aws_multi_account&lt;/a&gt;&lt;/p&gt;

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

├── deployment       # Terragrunt configuration files
│   ├── accounts
│   │   ├── sandbox1
│   │   │   ├── account.hcl
│   │   │   └── eu-west-1
│   │   │       ├── infra
│   │   │       │   └── terragrunt.hcl
│   │   │       └── region.hcl
│   │   ├── sandbox2
│   │   │   ├── account.hcl
│   │   │   └── eu-west-1
│   │   │       ├── infra
│   │   │       │   └── terragrunt.hcl
│   │   │       └── region.hcl
│   │   └── sandbox3
│   │       ├── account.hcl
│   │       ├── eu-north-1
│   │       │   ├── infra
│   │       │   │   └── terragrunt.hcl
│   │       │   └── region.hcl
│   │       └── eu-west-1
│   │           ├── infra
│   │           │   └── terragrunt.hcl
│   │           └── region.hcl
│   └── terragrunt.hcl
└── modules       # Terraform module for S3 - bucket
    ├── main.tf
    ├── outputs.tf
    ├── s3.tf
    └── vars.tf


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Terragrunt
&lt;/h3&gt;

&lt;p&gt;Configuration in &lt;code&gt;/deployment&lt;/code&gt; - folder defines which modules in &lt;code&gt;/modules&lt;/code&gt;- folder are deployed to which account and which region. My configuration creates S3 - bucket to &lt;code&gt;eu-west-1&lt;/code&gt; region in every sandbox - account. Sandbox3 gets additional bucket to &lt;code&gt;eu-north-1&lt;/code&gt; to show how this configuration can be extended to multiple regions. &lt;/p&gt;

&lt;p&gt;Most of the magic happens in &lt;code&gt;/deployment/terraform.hcl&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;generate&lt;/code&gt; block injects Terraform provider configuration into account - modules. Variables defined in account.hcl and region.hcl configuration files are used to implement dynamic &lt;code&gt;provider&lt;/code&gt; block. &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

generate "provider" {
    path    = "provider.tf"
    contents  = &amp;lt;&amp;lt;EOF
provider "aws" {
    region = "${local.aws_region}"
    assume_role {
        role_arn = "arn:aws:iam::${local.aws_account_id}:role/OrganizationAccountAccessRole"
    }


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Run
&lt;/h3&gt;

&lt;p&gt;To provision S3 bucket from /modules - folder to all sandbox - accounts in my configuration, I'm doing following:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

cd deployment/accounts
terragrunt run-all apply


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

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;terragrunt automatically calls &lt;code&gt;terragrunt init&lt;/code&gt; so it is not needed separately. &lt;/li&gt;
&lt;li&gt;If Terraform state bucket and DynamoDB table(as defined in /deployment/terragrunt.hcl) do not exist, terragrunt creates those.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Output
&lt;/h3&gt;

&lt;p&gt;I'm copy-pasting quite complete output from &lt;code&gt;terragrunt&lt;/code&gt; run here.&lt;/p&gt;

&lt;p&gt;Each module(here account / region) is provisioned one by one. We are not executing one &lt;strong&gt;"BIG terragrunt plan"&lt;/strong&gt; but instead set of separate terraform plans. By default these tf plans do not contain information about AWS account to which is being provisioned to. To make sure I understand correctly multi-account context here, I added account &lt;a href="https://www.hashicorp.com/blog/default-tags-in-the-terraform-aws-provider" rel="noopener noreferrer"&gt;default tags&lt;/a&gt; to provisioned resources. &lt;/p&gt;

&lt;p&gt;So, take a look:&lt;/p&gt;

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

➜  accounts git:(master) ✗ terragrunt run-all apply                                                  &amp;lt;aws:markus-sso-master&amp;gt; &amp;lt;region:eu-west-1&amp;gt;
INFO[0000] The stack at /terragrunt_aws_multi_account/deployment/accounts will be processed in the following order for command apply:
Group 1
- Module /terragrunt_aws_multi_account/deployment/accounts/sandbox1/eu-west-1/infra
- Module /terragrunt_aws_multi_account/deployment/accounts/sandbox2/eu-west-1/infra
- Module /terragrunt_aws_multi_account/deployment/accounts/sandbox3/eu-north-1/infra
- Module /terragrunt_aws_multi_account/deployment/accounts/sandbox3/eu-west-1/infra

Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n) y

Initializing the backend...
Initializing the backend...
Initializing the backend...
Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.31.0

Terraform has been successfully initialized!

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.31.0

Terraform has been successfully initialized!
- Using previously-installed hashicorp/aws v5.31.0

Terraform has been successfully initialized!
- Using previously-installed hashicorp/aws v5.31.0

Terraform has been successfully initialized!

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.bucket will be created
  + resource "aws_s3_bucket" "bucket" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = (known after apply)
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = "sandbox3-dev-eu-north-1"
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = true
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = {
          + "account"     = "333333333333"
          + "environment" = "dev"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bucket_name = (known after apply)
aws_s3_bucket.bucket: Creating...

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.bucket will be created
  + resource "aws_s3_bucket" "bucket" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = (known after apply)
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = "sandbox2-dev-eu-west-1"
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = true
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = {
          + "account"     = "222222222222"
          + "environment" = "dev"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bucket_name = (known after apply)

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.bucket will be created
  + resource "aws_s3_bucket" "bucket" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = (known after apply)
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = "sandbox1-dev-eu-west-1"
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = true
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = {
          + "account"     = "1111111111111"
          + "environment" = "dev"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bucket_name = (known after apply)

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.bucket will be created
  + resource "aws_s3_bucket" "bucket" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = (known after apply)
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = "sandbox3-dev-eu-west-1"
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = true
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = {
          + "account"     = "333333333333"
          + "environment" = "dev"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bucket_name = (known after apply)
aws_s3_bucket.bucket: Creation complete after 1s [id=sandbox3-dev-eu-north-120231228125652554400000001]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

bucket_name = "sandbox3-dev-eu-north-120231228125652554400000001"
aws_s3_bucket.bucket: Creating...
aws_s3_bucket.bucket: Creating...
aws_s3_bucket.bucket: Creating...
aws_s3_bucket.bucket: Creation complete after 3s [id=sandbox1-dev-eu-west-120231228125655120500000001]
aws_s3_bucket.bucket: Creation complete after 3s [id=sandbox2-dev-eu-west-120231228125655134100000001]
aws_s3_bucket.bucket: Creation complete after 3s [id=sandbox3-dev-eu-west-120231228125655260000000001]
Releasing state lock. This may take a few moments...
Releasing state lock. This may take a few moments...

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

bucket_name = "sandbox1-dev-eu-west-120231228125655120500000001"

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

bucket_name = "sandbox2-dev-eu-west-120231228125655134100000001"
Releasing state lock. This may take a few moments...

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

bucket_name = "sandbox3-dev-eu-west-120231228125655260000000001"
➜  accounts git:(master) ✗                                                      


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

&lt;/div&gt;

&lt;p&gt;After &lt;code&gt;terragrunt run-all&lt;/code&gt; finishes, every sandbox account has S3 bucket in specified region. &lt;/p&gt;

&lt;p&gt;There isn't any final extra output by terragrunt which would combine results on how many resources were created/updated in total. &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Ok, that's all! I wanted to test Terragrunt and how it would perform on multi-account environment. Based on this trial, few key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;(Lack of )concurrency&lt;/strong&gt;. Terraform/Terragrunt combination handles modules(here AWS accounts) sequentially, one after another. For few accounts this would work but for hundreds of accounts you may want solution with parallel deployments(AWS Control Tower, ADF etc)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unclear TF plan&lt;/strong&gt;. I love terraform plan. It is very precise, very clear on the changes it is going to perform. Adding multi-account structure with Terragrunt is not by default clear on which AWS account it is working on. Also, running &lt;code&gt;terragrunt run-all destroy&lt;/code&gt; just warns that it is going to run destroy on all &lt;code&gt;/deployment/accounts/&lt;/code&gt; - folder. Resource specific information is shown only after destroy - command has been approved and run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think &lt;code&gt;terragrunt&lt;/code&gt; is very useful tool for handling example separate dev / qa / prod environments in Terraform. But for multi-account management tasks it may turn out too lightweight. &lt;/p&gt;

&lt;p&gt;I'm keeping my eyes on Hashicorp side and what is happening on &lt;a href="https://www.hashicorp.com/blog/terraform-stacks-explained" rel="noopener noreferrer"&gt;Terraform stacks&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>cloudskills</category>
      <category>aws</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Timezone aware Lambda cron schedule</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Fri, 28 Oct 2022 11:22:07 +0000</pubDate>
      <link>https://dev.to/aws-builders/timezone-aware-lambda-cron-schedule-272k</link>
      <guid>https://dev.to/aws-builders/timezone-aware-lambda-cron-schedule-272k</guid>
      <description>&lt;p&gt;The most straightforward way to trigger Lambda function on schedule is to create scheduled Eventbridge Rule. With Eventbridge Rule, one caveat is that all scheduled events are triggered using &lt;code&gt;UTC +0&lt;/code&gt; time zone. In many use cases that does not matter but there are tasks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scheduled data loading&lt;/li&gt;
&lt;li&gt;Backups&lt;/li&gt;
&lt;li&gt;Environment scans and other "housekeeping tasks"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where working with static &lt;code&gt;UTC +0&lt;/code&gt; is at least little bit annoying. You know how it goes, Lambda task that used to trigger 8:00 local time is one morning triggering 9:00 local time. &lt;/p&gt;

&lt;p&gt;To implement timezone aware cron scheduled Lambda, &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-maintenance.html" rel="noopener noreferrer"&gt;Systems Manager Maintenance Window&lt;/a&gt; can be used. Maintenance Window is a technology overkill for such a simple task and I recommend to also check other features it offers for task scheduling. However, for now we are happy with just basic Lambda triggering.&lt;/p&gt;

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

&lt;p&gt;Full Cloudformation with timezone aware Lambda trigger can be downloaded &lt;a href="https://github.com/markymarkus/cloudformation/blob/master/tz-lambda-cron/tz-lambda-cron.yml" rel="noopener noreferrer"&gt;HERE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Main resources for implementation are described as follows:&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;StartWindow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::SSM::MaintenanceWindow&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;AllowUnassociatedTargets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;Cutoff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="na"&gt;Duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CronTrigger&lt;/span&gt;
      &lt;span class="na"&gt;Schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cron(0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;18&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MON-FRI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*)'&lt;/span&gt;
      &lt;span class="na"&gt;ScheduleTimezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Europe/Helsinki'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minimum precision of &lt;code&gt;Schedule&lt;/code&gt; is one minute and scheduled tasks are using &lt;code&gt;ScheduleTimezone&lt;/code&gt; (here Europe/Helsinki).&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;StartTask&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::SSM::MaintenanceWindowTask&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StartTask&lt;/span&gt;
      &lt;span class="na"&gt;Priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;TaskArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;__LAMBDA_FUNCTION_ARN__&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;WindowId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;StartWindow&lt;/span&gt;
      &lt;span class="na"&gt;ServiceRoleArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;AutomationExecutionRole.Arn&lt;/span&gt;
      &lt;span class="na"&gt;TaskType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LAMBDA&lt;/span&gt;    &lt;span class="c1"&gt;# also STEP_FUNCTIONS&lt;/span&gt;
      &lt;span class="na"&gt;TaskInvocationParameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;MaintenanceWindowLambdaParameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Payload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Base64&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{"message":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Hello&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;World!"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MaintenanceWindowTask&lt;/code&gt; defines a task that is performed on &lt;code&gt;MaintenanceWindow&lt;/code&gt; cron schedule. Outside of Systems Manager automations, &lt;code&gt;TaskType&lt;/code&gt; covers integrations to &lt;strong&gt;LAMBDA&lt;/strong&gt; and &lt;strong&gt;STEP_FUNCTIONS&lt;/strong&gt;. Other AWS or external services can be triggered with &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-documents.html" rel="noopener noreferrer"&gt;Systems Manager automation runbooks&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda function &lt;code&gt;Payload&lt;/code&gt; must be Base64 encoded string.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That's it! As Finland sets clocks back one hour in 30.10.2022, so does my Lambda cron schedule.&lt;/p&gt;

</description>
      <category>cloudskills</category>
      <category>aws</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Debugging failed Eventbridge invocation</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Sat, 01 Oct 2022 14:54:21 +0000</pubDate>
      <link>https://dev.to/aws-builders/debugging-failed-eventbridge-invocation-3ih6</link>
      <guid>https://dev.to/aws-builders/debugging-failed-eventbridge-invocation-3ih6</guid>
      <description>&lt;p&gt;When Eventbridge tries to send an event to a target and the delivery fails, by default only way to notice this is from &lt;code&gt;FailedInvocation&lt;/code&gt; Cloudwatch Metric. The metric itself is not enough to get the actual reason why the event delivery is failing. &lt;br&gt;
In general there are two options to debug &lt;code&gt;FailedInvocaton&lt;/code&gt; issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Debug on the resource level. If your Eventbridge Rule is targeting Lambda function, try to search for failed Lambda invocations from Cloudtrail logs.&lt;/li&gt;
&lt;li&gt;Forward failed deliveries to DLQ(Dead Letter Queue). &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On this blog post I'm showing how to configure DLQ to Eventbridge target and how to write error logs to Cloudwatch Logs.&lt;/p&gt;

&lt;p&gt;You can get full template from: &lt;a href="https://github.com/markymarkus/cloudformation/blob/master/eventbridge-debug-dlq/template.yml" rel="noopener noreferrer"&gt;https://github.com/markymarkus/cloudformation/blob/master/eventbridge-debug-dlq/template.yml&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Walkthrough
&lt;/h1&gt;

&lt;p&gt;We are starting with very basic &lt;code&gt;AWS::Events::Rule&lt;/code&gt; on account 111111111111 which forwards events from &lt;code&gt;custom.source&lt;/code&gt; to event bus on account 222222222222. &lt;code&gt;FailedInvocation&lt;/code&gt; metrics shows that all the invocations are failing.(See Fig.1)&lt;/p&gt;
&lt;h2&gt;
  
  
  Enable error logging
&lt;/h2&gt;

&lt;p&gt;To get better understanding why events are not reaching a target eventbus, following resources are added: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configure DLQ(SQS) for failing target.&lt;/li&gt;
&lt;li&gt;Set Eventbridge Target retry count to 0. Depending on the error type involved, Eventbridge retries to send event 24h before failing and sending the event to DLQ. Setting retry count to zero ensures that failed event is sent to DLQ asap.&lt;/li&gt;
&lt;li&gt;Create Lambda function to get error messages from the DLQ(SQS) queue and writing error logs to Cloudwatch Logs.&lt;/li&gt;
&lt;/ul&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%2F91xms7zsrheqn6c08ezu.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%2F91xms7zsrheqn6c08ezu.png" alt="Architecture" width="402" height="291"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig.1 Architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And this is how the configuration looks in &lt;a href="https://github.com/markymarkus/cloudformation/blob/master/eventbridge-debug-dlq/template.yml" rel="noopener noreferrer"&gt;Cloudformation template&lt;/a&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;CustomEventsRule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Events::Rule&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;EventBusName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CustomEventBus.Arn&lt;/span&gt;
      &lt;span class="na"&gt;EventPattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;custom.source&lt;/span&gt;
      &lt;span class="na"&gt;State&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENABLED&lt;/span&gt;
      &lt;span class="na"&gt;Targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customtarget'&lt;/span&gt;
          &lt;span class="na"&gt;Arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:events:eu-west-1:222222222222:event-bus/default'&lt;/span&gt;
          &lt;span class="na"&gt;RetryPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;MaximumRetryAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
          &lt;span class="na"&gt;DeadLetterQueue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;DLQueue.Arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After dead letter queue setup is in place, wait for next failing invocation and open DLQ handler Lambda's execution logs from Cloudwath Logs. &lt;code&gt;ERROR_MESSAGE&lt;/code&gt; and &lt;code&gt;ERROR_CODE&lt;/code&gt; fields have human readable reason why the sending is failing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;....&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"messageAttributes"&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;/span&gt;&lt;span class="nl"&gt;"RULE_ARN"&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;/span&gt;&lt;span class="nl"&gt;"stringValue"&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:events:eu-west-1:111111111111:rule/custom_event_bus/dev-eb-debug-CustomEventsRule-3GTDO9NDN1Q9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"stringListValues"&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;/span&gt;&lt;span class="nl"&gt;"binaryListValues"&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;/span&gt;&lt;span class="nl"&gt;"dataType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&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;/span&gt;&lt;span class="nl"&gt;"TARGET_ARN"&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;/span&gt;&lt;span class="nl"&gt;"stringValue"&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:events:eu-west-1:222222222222:event-bus/default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"stringListValues"&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;/span&gt;&lt;span class="nl"&gt;"binaryListValues"&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;/span&gt;&lt;span class="nl"&gt;"dataType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&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;/span&gt;&lt;span class="nl"&gt;"ERROR_MESSAGE"&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;/span&gt;&lt;span class="nl"&gt;"stringValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lack of permissions to invoke cross account target."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"stringListValues"&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;/span&gt;&lt;span class="nl"&gt;"binaryListValues"&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;/span&gt;&lt;span class="nl"&gt;"dataType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&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;/span&gt;&lt;span class="nl"&gt;"ERROR_CODE"&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;/span&gt;&lt;span class="nl"&gt;"stringValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NO_PERMISSIONS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"stringListValues"&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;/span&gt;&lt;span class="nl"&gt;"binaryListValues"&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;/span&gt;&lt;span class="nl"&gt;"dataType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&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;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time the delivery was failing because of terminated Eventbridge Policy on the receiving AWS account.&lt;/p&gt;

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

&lt;p&gt;In general DLQs require some logic to handle failed events. Adding alarm for failed Eventbridge invocation and logging via DLQ is the first step to understand if that logic should be developed further. &lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudskills</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Tracking grocery price trends on AWS - Part 2 - Analytics</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Thu, 28 Jul 2022 05:22:00 +0000</pubDate>
      <link>https://dev.to/markymarkus/tracking-grocery-price-trends-on-aws-part-2-analytics-g5d</link>
      <guid>https://dev.to/markymarkus/tracking-grocery-price-trends-on-aws-part-2-analytics-g5d</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/markymarkus/tracking-grocery-price-trends-on-aws-part-1-ingestion-pipeline-3gpm"&gt;Part 1&lt;/a&gt;, we implemented ingestion pipeline for grocery receipts and if you have followed through the instructions, you should now have test data from two grocery receipts extracted to JSON files in S3.&lt;/p&gt;

&lt;p&gt;In this second part, I'm going to show you basic steps on how to run analysis on the extracted grocery data. AWS services we are going to use are &lt;a href="https://aws.amazon.com/athena" rel="noopener noreferrer"&gt;AWS Athena&lt;/a&gt; and &lt;a href="https://aws.amazon.com/quicksight/" rel="noopener noreferrer"&gt;Amazon Quicksight&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Setup Athena
&lt;/h2&gt;

&lt;p&gt;First we are creating Athena table for extracted data. Data schema is a simple one. Just remember to change the bucket name to match your case and import partitioned data with MSCK REPAIR.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE EXTERNAL TABLE grocery_items (
    name string,
    price float,
    currency string,
    unit string,
    date string
  )
PARTITIONED BY (store string)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://my-grocery-tracking-output/' 

MSCK REPAIR TABLE grocery_items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query the data with &lt;code&gt;SELECT * FROM grocery_items&lt;/code&gt; and you should get 37 rows of grocery data. Nice, that means the solution is end-to-end working.&lt;/p&gt;

&lt;p&gt;Next we need &lt;strong&gt;more data&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backfill the purchase history
&lt;/h2&gt;

&lt;p&gt;If you were to start collecting receipts from this day onwards, it can be weeks or more likely months before there is a price change on the item. Luckily it's possible to get grocery purchase receipt history to see price changes leading to the current point.&lt;/p&gt;

&lt;p&gt;S-Group and Kesko, biggest players in Finnish grocery market, both have applications with opt-in services for digitised receipts(apps: &lt;a href="https://www.s-kanava.fi/asiakasomistajuus/palvelut/s-mobiili/" rel="noopener noreferrer"&gt;S-Mobiili&lt;/a&gt;, &lt;a href="https://plussa.fi/k-plussa/palvelut" rel="noopener noreferrer"&gt;K-Ruoka&lt;/a&gt;). Both applications can export receipts in PDF. All we need to do is to shovel the PDF receipts into the ingestion pipeline and visualise the results. You can also scan/photo receipts into JPG and that is working as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some words about the data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In analysis, I have treated grocery item name as &lt;em&gt;unique identifier&lt;/em&gt;. For example, &lt;code&gt;KAURAJUOMA&lt;/code&gt;(Oat Milk) in following trends means The Oat Milk. It is very possible that actual receipt line &lt;code&gt;KAURAJUOMA&lt;/code&gt; can refer to BrandA Kaurajuoma(1.5e) or BrandB Kaurajuoma(1.95e). In such case, same item name in the data would have two different prices. That is very possible anomaly in the price item analysis if your buying pattern is not consistent. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If there is a data point missing on the graph that doesn't mean there has not been a price change. It just indicates there is no data on &lt;em&gt;my&lt;/em&gt; receipts about the price change.  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Query the grocery data
&lt;/h2&gt;

&lt;p&gt;I have used S-Group's app and Omat Ostot service for about two years and on the following examples I am showing some trends based on that data. Basically I have first exported Omat Ostot - grocery receipts to PDF and then synced everything to the ingestion pipeline input - bucket.&lt;/p&gt;

&lt;p&gt;I have about 6000 rows of data in &lt;code&gt;grocery_items&lt;/code&gt; table. &lt;/p&gt;

&lt;p&gt;First query is to check which grocery items are occurring most in the data. Which items are purchased most frequently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT name,COUNT(name) AS item_line_count FROM items GROUP BY name ORDER BY item_line_count DESC

1   KAURAJUOMA  161
2   LUOMU RASVATON MAITO    125
3   MAITOJUOMA LAKTON RASVATO   124
4   PAPRIKA PUNAINEN IRTO   87
5   PEHMEÄ MAITORAHKA  84
6   RUISPUIKULAT    79
7   KURKKU SUOMI    79
8   BANAANI LUOMU   74
9   TOMAATTI SUOMI  73
10  AVOKADOPUSSI    70
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Huh, no beer on that list. Just milk, rye bread and vegetables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualize the price trends
&lt;/h2&gt;

&lt;p&gt;Next, &lt;a href="https://docs.aws.amazon.com/quicksight/latest/user/create-a-data-set-athena.html" rel="noopener noreferrer"&gt;I created a dataset from Athena&lt;/a&gt; grocery data to Amazon Quicksight. Following graphs are created from the dataset, data filtered to S-Market Kaleva supermarket. &lt;/p&gt;

&lt;p&gt;First price trends are for dairy and meat products:&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%2F73apxwnatxats0uxbwks.jpg" 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%2F73apxwnatxats0uxbwks.jpg" alt="Image description" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, vegetables and fruits. Seasonal price fluctuation is clearly the trend with the fresh vegetables. Bananas though..&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%2F1w23ktxwsbx3tezf1b8h.jpg" 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%2F1w23ktxwsbx3tezf1b8h.jpg" alt="Image description" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cereal products, bread, flour, misc. Coffee was a first product that caught my attention of the price increases early spring. &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%2Fxp5j9aarciinxlh6kfyl.jpg" 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%2Fxp5j9aarciinxlh6kfyl.jpg" alt="Image description" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Based on these, talk about grocery inflation 2022 is a real deal. Prices are increasing in almost every category of tracked grocery products. It is interesting to see how the trends will be a few months down the line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By following the steps detailed in these posts, you can implement described receipt ingest pipeline and start doing analyses on the data. &lt;/p&gt;

&lt;p&gt;For this quick exercise, I wanted to just focus on the grocery inflation. Another interesting angle for the data would be to analyse prices between grocery chains. That would require  understanding of the grocery range in each market and how to match references in the data for meaningful comparison.. And much more receipts.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>analytics</category>
      <category>cloudskills</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Tracking grocery price trends on AWS - Part 1 - Ingestion pipeline</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Thu, 28 Jul 2022 05:21:00 +0000</pubDate>
      <link>https://dev.to/markymarkus/tracking-grocery-price-trends-on-aws-part-1-ingestion-pipeline-3gpm</link>
      <guid>https://dev.to/markymarkus/tracking-grocery-price-trends-on-aws-part-1-ingestion-pipeline-3gpm</guid>
      <description>&lt;p&gt;During the first half of 2022 inflation has been increasing prices in every category of goods and services. Inflation is mentioned every day on the news but without manual bookkeeping it can be hard to notice how the inflation affects daily cost of living. Small increases, accumulated, can make a big change on monthly or yearly budget.&lt;/p&gt;

&lt;p&gt;How to track price changes for daily food essentials like milk and bread? That is the main question we are going to tackle in this blog posting. Food supplies that are bought again and again on every grocery run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;To track grocery price trends, I came up with an idea of gathering the pricing data from the grocery receipts. All the needed data is there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Item name&lt;/li&gt;
&lt;li&gt;Price per item&lt;/li&gt;
&lt;li&gt;Purchase date&lt;/li&gt;
&lt;li&gt;Shop name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parsing and analysing the collection of grocery receipts provides the needed data for tracking the grocery price trends. All we need is an automated pipeline to extract the data from the receipts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1&lt;/strong&gt; of the blog post will cover how to setup the receipt ingestion pipeline. After we have finished, we should have an automation to extract the data from a receipt to JSON object.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;&lt;a href="https://dev.to/markymarkus/tracking-grocery-price-trends-on-aws-part-2-analytics-g5d"&gt;Part 2&lt;/a&gt;&lt;/strong&gt;, I will present some ideas and results on price trend tracking. Data used on the analysis is from the close by supermarket, presenting price trends on the grocery items my household is buying regularly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Receipt data ingestion pipeline leverages serverless event driven workflow. An upload to S3 input bucket triggers receipt processing pipeline, resulting extracted grocery item data in JSON format to S3 output bucket. &lt;/p&gt;

&lt;p&gt;Main used AWS services are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/textract/" rel="noopener noreferrer"&gt;Amazon Textract&lt;/a&gt; detects and extracts lines of text from printed receipts.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/lambda/" rel="noopener noreferrer"&gt;AWS Lambda&lt;/a&gt; is used for parsing the receipt data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Following actions are triggered on every receipt upload:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receipt .jpg or .pdf is uploaded to input bucket.&lt;/li&gt;
&lt;li&gt;Trigger Lambda passes receipt filename and SNS - topic to Amazon Textract.&lt;/li&gt;
&lt;li&gt;When Textract gets OCR data ready it publishes a Textract &lt;code&gt;JobId&lt;/code&gt; to the provided SNS - topic.&lt;/li&gt;
&lt;li&gt;Parser Lambda reads Textract result data, parses pricing data and writes result JSON to output bucket.&lt;/li&gt;
&lt;li&gt;(Part 2) Grocery receipt JSON data is analysed with Amazon Quicksight.&lt;/li&gt;
&lt;/ol&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%2Fddp7itxwwqqu5cwg2scz.jpg" 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%2Fddp7itxwwqqu5cwg2scz.jpg" alt="Image description" width="741" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing the receipt data
&lt;/h2&gt;

&lt;p&gt;Receipts from the following stores are supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S-Market&lt;/li&gt;
&lt;li&gt;Prisma&lt;/li&gt;
&lt;li&gt;Sale&lt;/li&gt;
&lt;li&gt;K-Market&lt;/li&gt;
&lt;li&gt;KCM&lt;/li&gt;
&lt;li&gt;K-Citymarket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Image below contains an example of a grocery receipt. Depending on grocery chain or supermarket, receipt format may have some nuances like using commas instead of dots for price decimal separator.&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%2Fkdgp8hvr1o3iihrf27zh.jpeg" 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%2Fkdgp8hvr1o3iihrf27zh.jpeg" alt="Image description" width="400" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When parsing the extracted receipt data, following variations on receipt item rows are implemented:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GREEN&lt;/strong&gt;. General information about the purchase. Store name and receipt date. &lt;br&gt;
&lt;strong&gt;YELLOW&lt;/strong&gt;. Basic grocery item line has item name(MAITOJUOMA LAKTON RASVATO aka. non-fat lactose free milk) and price.&lt;br&gt;
&lt;strong&gt;RED&lt;/strong&gt;. Alennus, discount entry. Receipt item can have reduced price for various reasons. This pipeline is for tracking grocery price trends, so we are happy with the full price.&lt;br&gt;
&lt;strong&gt;BLUE&lt;/strong&gt;. Multiple items on the same entry(EUR/KPL) or EUR/KG priced goods. Total price is on the first line but per item or per kilogram price on the second. Same as with RED items. Because our aim is to track price trends we will read item name from the first line and item price from the second line. That way we can track the price trend for example 1 Kg of bananas, not for daily banana purchase.&lt;/p&gt;

&lt;p&gt;For highlighted blocks on the receipt, pipeline outputs following JSON structure to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3://my-grocery-tracking-bucket-output/store=S-MARKET KALEVA PUH 0107671180/20191226-173900.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grocery store name is included to S3 prefix and used for data partitioning. More about that on the second part of the blog.&lt;br&gt;
JSON data contains one receipt item line per JSON object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20191226-173900.json:
{"name": "MAITOJUOMA LAKTON RASVATO", "price": 1.25, "currency": "EUR", "date": "2019-12-26 17:39:00"}
{"name": "100% KAURA 6KPL", "price": 1.59, "currency": "EUR", "date": "2019-12-26 17:39:00"}
{"name": "BANAANI LUOMU", "price": 1.79, "currency": "EUR", "date": "2019-12-26 17:39:00"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Deploy with Cloudformation
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/markymarkus/grocery-receipt-textract" rel="noopener noreferrer"&gt;Github repo for grocery-receipt-textract&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To try out the solution, you can deploy the ingestion pipeline from Cloudformation template. The template creates all needed AWS resources to your AWS account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/markymarkus/grocery-receipt-textract.git
cd grocery-receipt-textract
aws cloudformation package --s3-bucket cf-stage-sandbox-markus --output-template-file packaged.yaml --region eu-west-1 --template-file template.yml
aws cloudformation deploy --template-file packaged.yaml --stack-name dev-grocery-pipeline --parameter-overrides InputBucketName=my-grocery-tracking-bucket --capabilities CAPABILITY_IAM

# After the stack finishes, two buckets for receipts and pipeline outputs are created:
# Input = my-grocery-tracking-bucket
# Output = my-grocery-tracking-bucket-output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we will trigger the pipeline with some grocery receipt test data also included in the repo. Replace the bucket name with the input bucket name from the previous step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 sync test_data s3://my-grocery-tracking-bucket/ 
# Wait for about 1 min and check the results:
aws s3 ls s3://my-grocery-tracking-bucket-output/
#PRE store=K-Market Domus/
#PRE store=S-MARKET KALEVA PUH 0107671180/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That's it! We have successfully created grocery receipt ingestion pipeline. In Part 2, we will put the pipeline in action and see if there are any hints of inflation to be found from the extracted price data.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudskills</category>
      <category>analytics</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Using Athena to query multi-account Cloudwatch Logs</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Sat, 07 May 2022 10:10:10 +0000</pubDate>
      <link>https://dev.to/markymarkus/using-athena-to-query-multi-account-cloudwatch-logs-54j</link>
      <guid>https://dev.to/markymarkus/using-athena-to-query-multi-account-cloudwatch-logs-54j</guid>
      <description>&lt;p&gt;The scenario. We have multiple workloads and environments deployed to multi-account organization in AWS Organization. Cloudwatch Logs is used to to store logs from various services within a scope of a single AWS account. EC2s are pushing system logs, Lambda functions pushing execution logs and so on. &lt;/p&gt;

&lt;p&gt;In order to increase understanding on application logs, aggregating logs from separate AWS accounts to a single service or S3 bucket can be helpful. Depending on business, regulatory compliance may also oblige you to keep logs for certain amount of time. Handling long time log storage in a centralised account can help when implementing access control and retention schedules.&lt;/p&gt;

&lt;p&gt;This post demonstrates how to implement centralised log storage to multi-account organization in AWS Organizations. Main goal is to create cloud infrastructure to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store application logs from multiple AWS accounts to a single S3 bucket.&lt;/li&gt;
&lt;li&gt;Run ad-hoc queries agains data in S3 with Amazon Athena.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&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%2Fov2t8gg5ilab0fob1fzo.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%2Fov2t8gg5ilab0fob1fzo.png" alt="Logging overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The proposed solution uses Cloudwatch Logs for log ingestion on source accounts. Amazon Kinesis is used for log delivery to centralised storage and S3 for long term log data storage. Finally we will use Amazon Athena to run SQL queries against these logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;Cloudformation templates for the logging solution can be found here: &lt;a href="https://github.com/markymarkus/cloudformation/tree/master/centralised-cloudwatch-logs" rel="noopener noreferrer"&gt;https://github.com/markymarkus/cloudformation/tree/master/centralised-cloudwatch-logs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;logging-account-template.yml&lt;/code&gt; provisions infrastructure for Log Storage Account. Template parameter &lt;code&gt;OrganizationId&lt;/code&gt; is used on &lt;a href="https://aws.amazon.com/about-aws/whats-new/2022/01/amazon-cloudwatch-logs-aws-organizations-subscriptions/" rel="noopener noreferrer"&gt;Cloudwatch Logs Destination access policy&lt;/a&gt; to allow log delivery from AWS Organizations member accounts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;member-account-template.yml&lt;/code&gt; demonstrates how to create Subscription Filter to Cloudwatch Logs. You can create this also on the same account where &lt;code&gt;logging-account-template.yml&lt;/code&gt; is deployed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Log Source Accounts
&lt;/h3&gt;

&lt;p&gt;Source accounts have workloads producing and ingesting logs to Cloudwatch Logs. Each log group is storing logging events from a specific source. In our example:&lt;br&gt;
&lt;code&gt;/var/log/messages&lt;/code&gt; stores system logs from EC2 instances.&lt;br&gt;
&lt;code&gt;/aws/lambda/hello-world&lt;/code&gt; stores logs from Hello World Lambda function.&lt;br&gt;
Having consistent naming conventions for log groups will come handy when we run Athena queries against the data. Queries like '/var/log/messages of every EC2 instance in every AWS account of Organization' are depended on consistent naming.  &lt;/p&gt;

&lt;p&gt;Logs are sent from a member account to a receiving centralised destination in Log Storage Account through a &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html" rel="noopener noreferrer"&gt;subscription filter&lt;/a&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Log Storage Account
&lt;/h3&gt;

&lt;p&gt;Log Storage Account receives and prepares logging data for Athena and stores logs to S3 bucket. Logs are stored in one JSON record per line format. Supported by Athena and easy to export other services and tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transforming log events
&lt;/h3&gt;

&lt;p&gt;By default, Firehose writes JSON records in a stream to S3 bucket without separators or new lines. This would work if we would not be using Athena to run queries. Athena requires each JSON record to be represented in a separate line. &lt;br&gt;
To split JSON records we are using &lt;a href="https://docs.aws.amazon.com/firehose/latest/dev/data-transformation.html" rel="noopener noreferrer"&gt;Firehose transformation Lambda&lt;/a&gt;. Lambda function reads records batch and adds a new line character &lt;code&gt;+ "\n"&lt;/code&gt; after every record. Processed records are written back to Firehose stream which finally delivers logs to S3 bucket. &lt;/p&gt;

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

output = []
for record in event['records']:
    payload = base64.b64decode(record['data'])
    striodata = BytesIO(payload)
    with gzip.GzipFile(fileobj=striodata, mode='r') as f:
        payload = f.read().decode("utf-8")

    # Add newline to each record
    output_record = {
        'recordId': record['recordId'],
        'result': 'Ok',
        'data': base64.b64encode((payload + "\n").encode("utf-8"))
    }
    output.append(output_record)


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

&lt;/div&gt;
&lt;h2&gt;
  
  
  Running Athena queries
&lt;/h2&gt;

&lt;p&gt;At this point we have application logs in S3 bucket in Log Storage account. Kinesis is a time based stream so each file contains logging from multiple Source AWS accounts and log groups: &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%2Ft6qbyqqwnyeqc0i1ephv.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%2Ft6qbyqqwnyeqc0i1ephv.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get Athena queries running, first create external table pointing to the data:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

CREATE EXTERNAL TABLE logs (
    messageType string,
    owner string,
    logGroup string,
    subscriptionFilters string,
    logEvents array&amp;lt;struct&amp;lt;id:string,
              timestamp:string,
              message:string
              &amp;gt;&amp;gt;
  )           
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://BUCKET_NAME/logs/year=2022/month=05/day=05/' 


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

&lt;/div&gt;

&lt;p&gt;logEvents are stored in array structure. In order to query Cloudwatch Logs fields within an array, you need to &lt;a href="https://docs.aws.amazon.com/athena/latest/ug/flattening-arrays.html" rel="noopener noreferrer"&gt;UNNEST&lt;/a&gt; logEvents. Here are couple of queries to get you started:  &lt;/p&gt;

&lt;p&gt;First query returns latest streamed logs from Source Accounts:&lt;/p&gt;

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

SELECT owner,loggroup,n.message FROM logs
CROSS JOIN UNNEST(logs.logevents) AS t (n)
LIMIT 20
######
1   111111111111    /var/log/messages   May 5 03:07:48 ip-10-0-24-56 dhclient[2085]: XMT: Solicit on eth0, interval 119340ms.
2   222222222222    /aws/lambda/hello-world-lambda  START RequestId: fcc9e873-d4c1-4ca3-a7de-fd5490300740 Version: $LATEST
3   222222222222    /aws/lambda/hello-world-lambda  [DEBUG] 2022-05-05T03:07:50.972Z fcc9e873-d4c1-4ca3-a7de-fd5490300740 {'version': '0', 'id': ....
4   222222222222    /aws/lambda/hello-world-lambda  [DEBUG] 2022-05-05T03:07:50.973Z fcc9e873-d4c1-4ca3-a7de-fd5490300740 Hello World!
5   111111111111    /aws/lambda/lambda-writer-LambdaFunction    START RequestId: 38fb9b6b-dbea-4875-91cc-cf1dd5b36ab9 Version: $LATEST
6   111111111111    /aws/lambda/lambda-writer-LambdaFunction    [DEBUG] 2022-05-05T03:08:16.81Z 38fb9b6b-dbea-4875-91cc-cf1dd5b36ab9
7   111111111111    /var/log/messages   May 5 11:40:01 ip-10-0-24-56 systemd: Created slice User Slice of root.
8   111111111111    /var/log/messages   May 5 11:40:01 ip-10-0-24-56 systemd: Started Session 169 of user root. 



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

&lt;/div&gt;

&lt;p&gt;Next SQL query return log event count for each logging source(Log group):&lt;/p&gt;


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

&lt;p&gt;SELECT owner,loggroup,count(*) FROM logs&lt;br&gt;
CROSS JOIN UNNEST(logs.logevents) AS t (n)&lt;br&gt;
GROUP BY owner,loggroup&lt;/p&gt;

&lt;p&gt;1   111111111111    /var/log/secure 429&lt;br&gt;
2   111111111111    /var/log/messages   1670&lt;br&gt;
3   222222222222    /aws/lambda/hello-world-lambda  5764&lt;br&gt;
4   111111111111    /aws/lambda/lambda-writer-LambdaFunction    7198&lt;br&gt;
...&lt;/p&gt;

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

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Conclusion&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;If you are in process of building and planning a logging strategy, this solution can be a good starting point. You can collect Cloudwatch Logs from multiple accounts and regions to a single S3 bucket. Athena queries can be executed against the consolidated logging data. I encourage you to experiment with SQL queries to the logging data. Analysing source specific logging patterns and event amounts may help you to improve an overall log management process. &lt;/p&gt;

&lt;p&gt;Thanks for reading. &lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudskills</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Paginate direct AWS SDK calls with AWS Step Functions</title>
      <dc:creator>Markus Toivakka</dc:creator>
      <pubDate>Fri, 18 Mar 2022 09:16:46 +0000</pubDate>
      <link>https://dev.to/markymarkus/paginate-direct-aws-sdk-calls-with-aws-step-functions-2m3l</link>
      <guid>https://dev.to/markymarkus/paginate-direct-aws-sdk-calls-with-aws-step-functions-2m3l</guid>
      <description>&lt;p&gt;AWS Step Functions &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/supported-services-awssdk.html" rel="noopener noreferrer"&gt;AWS SDK integration&lt;/a&gt; lets you call huge selection of AWS services directly from your Step Functions workflow.&lt;/p&gt;

&lt;p&gt;For API calls that can return a large list of items, APIs are returning by default only the first set of results. For example, S3 list objects response returns by default max. 1000 objects. Rest of the results must be requested by providing pagination token on the request.&lt;/p&gt;

&lt;p&gt;For data processing, pagination is very useful and mandatory pattern. Dividing a result set to fixed size pages makes it easier to build for example meaningful retry logic for error handling. Also, executing partial result sets &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-parallel-state.html" rel="noopener noreferrer"&gt;in parallel&lt;/a&gt; can improve a workflow running time significantly.   &lt;/p&gt;

&lt;p&gt;In this example I am showing how listing objects(&lt;code&gt;arn:aws:states:::aws-sdk:s3:listObjectsV2&lt;/code&gt;) on S3 bucket and then triggering a processing step with batch of S3 objects would be implemented in Step Functions ASL(Amazon State Language). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; some AWS APIs use &lt;code&gt;NextToken&lt;/code&gt; to paginate the results. Workflow with pagination is still same as I am covering next with &lt;code&gt;ContinuationToken&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to implement pagination with ASL.
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/markymarkus/ab8dd0511374eb84d0efd872dc2c0291" rel="noopener noreferrer"&gt;Cloudformation template&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This example shows very simple flow of listing objects on S3 buckets and then triggering the processing step. &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%2Feggsy3qc9f7j7jd3aeou.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%2Feggsy3qc9f7j7jd3aeou.png" alt="workflow" width="400" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below is the ASL definition. &lt;code&gt;BatchSize&lt;/code&gt; parameter controls how many S3 objects are included in each processing batch. We keep requesting new batches as long as the response is including &lt;code&gt;IsTruncated: true&lt;/code&gt;. Size of last object batch is below &lt;code&gt;BatchSize&lt;/code&gt; with &lt;code&gt;IsTruncated: false&lt;/code&gt; so we can finish processing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "Comment": "List S3 objects.",
    "StartAt": "list_s3",
    "States": {
        "list_s3": {
            "Comment": "Get first batch of objects.",
            "Type": "Task",
            "Resource": "arn:aws:states:::aws-sdk:s3:listObjectsV2",
            "ResultPath": "$.s3_objects",
            "Parameters": {
                "Bucket": "${BucketName}",
                "MaxKeys": ${BatchSize}
            },
            "Next": "process_s3_objects"
            },
        "process_s3_objects": {
            "Comment": "Processing logic. Now we just wait.",
            "Type": "Wait",
            "Seconds": 2,
            "Next": "check_if_all_listed"
            },
        "check_if_all_listed": {
            "Type": "Choice",
            "Choices": [
                {
                "Variable": "$.s3_objects.IsTruncated",
                "BooleanEquals": false,
                "Next": "success_state"
                }
                ],
            "Default": "list_s3_with_continuation_token"
            },
        "list_s3_with_continuation_token": {
            "Comment": "Get next batch of objects. Provide ContinuationToken in the request.",
            "Type": "Task",
            "Resource": "arn:aws:states:::aws-sdk:s3:listObjectsV2",
            "ResultPath": "$.s3_objects",
            "Parameters": {
                "Bucket": "${BucketName}",
                "MaxKeys": ${BatchSize},
                "ContinuationToken.$": "$.s3_objects.NextContinuationToken"
                },
            "Next": "process_s3_objects"
            },
        "success_state": {
            "Type": "Succeed"
            }
     }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;AWS Step Functions is a perfect fit for coordinating workflows and orchestrating AWS services. I strongly recommend building library of good templates for getting a running start for adapting it to your use cases.   &lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudskills</category>
    </item>
  </channel>
</rss>
