{"id":875,"date":"2025-06-17T11:37:17","date_gmt":"2025-06-17T10:37:17","guid":{"rendered":"https:\/\/livingdevops.com\/?p=875"},"modified":"2025-06-17T11:51:10","modified_gmt":"2025-06-17T10:51:10","slug":"aws-lambda-tutorial-python-terraform","status":"publish","type":"post","link":"https:\/\/livingdevops.com\/devops\/aws-lambda-tutorial-python-terraform\/","title":{"rendered":"AWS Lambda Tutorial: Build a real world devops project with Python &amp; Terraform"},"content":{"rendered":"\n<p><strong><mark style=\"background-color:rgba(0, 0, 0, 0);color:#00d084\" class=\"has-inline-color\">Building a production Lambda function that monitors IAM access keys and sends automated email alerts using boto3 and AWS&nbsp;SES.<\/mark><\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario<\/h3>\n\n\n\n<p>In my project, we have over 10+ users with access keys. Most of the access keys were 300\u2013400 days old.<\/p>\n\n\n\n<p>Having access keys lying around for a long period poses a security risk, which is unacceptable. They\u2019re not just a minor security issue\u200a\u2014\u200athey\u2019re multimillion-dollar liabilities.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Learn AWS Lambda by Solving a $1M Security&nbsp;Problem<\/h3>\n\n\n\n<p>The issue I can see is that no one remembers to rotate the access keys, so I thought of building a Lambda function that could remind the team to rotate the keys after a certain number of days.<\/p>\n\n\n\n<p>Different organizations enforce varying rotation policies\u200a\u2014\u200asome require rotation every 30 days, others allow 60 or 90 days before access keys must be refreshed.<\/p>\n\n\n\n<p>My solution needed to accommodate these different requirements while being easy to deploy and maintain.<\/p>\n\n\n\n<p>\ud83d\udc49 <strong><a href=\"https:\/\/livingdevops.com\/aws\/a-complete-guide-to-aws-lambda-with-real-world-use-case\/\" target=\"_blank\" rel=\"noreferrer noopener\">If you are new to Lambda functions, then read this blog post.<\/a><\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What to expect in this blog&nbsp;post<\/h3>\n\n\n\n<p>In this blog post, I\u2019ll walk you through the complete implementation of an automated access key rotation reminder system. You\u2019ll learn how to build a Lambda function that monitors key ages, sends targeted notifications, and helps your team maintain robust security hygiene without the overhead of manual tracking.<\/p>\n\n\n\n<p>\u2705 <a href=\"https:\/\/github.com\/akhileshmishrabiz\/Devops-zero-to-hero\/tree\/main\/AWS-Projects\/lambda-iam-key-rotation\" rel=\"noreferrer noopener\" target=\"_blank\"><strong>You can find the entire code for this blog post on my public GitHub repo<\/strong><\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Writing the Python&nbsp;code<\/h3>\n\n\n\n<p>I used AWS Python SDK, boto3, for the automation. Here is the approach I took.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>List the users and access keys<\/li>\n\n\n\n<li>Calculate the age<\/li>\n\n\n\n<li>Compare it with age standards, and determine if this key should be rotated.<\/li>\n\n\n\n<li>If the key should be rotated, build the email body that includes the link to the user<\/li>\n\n\n\n<li>Send the email using AWS SES(Simple Email Service)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Building the&nbsp;code<\/h3>\n\n\n\n<p>Before we deploy a fully functioning Lambda, I will build and test the code locally.<\/p>\n\n\n\n<p>Note: Also, configure the AWS credentials -&gt; <code>aws configure<\/code> locally<\/p>\n\n\n\n<p>I will use boto3, datetime, and email Python modules. datetime and email are Python built-in modules, but we need to install boto3, which is the AWS-managed library.<\/p>\n\n\n\n<script async src=\"https:\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-2356534591331561\"\n     crossorigin=\"anonymous\"><\/script>\n<ins class=\"adsbygoogle\"\n     style=\"display:block; text-align:center;\"\n     data-ad-layout=\"in-article\"\n     data-ad-format=\"fluid\"\n     data-ad-client=\"ca-pub-2356534591331561\"\n     data-ad-slot=\"3134096072\"><\/ins>\n<script>\n     (adsbygoogle = window.adsbygoogle || []).push({});\n<\/script>\n\n\n\n<h4 class=\"wp-block-heading\">Install the&nbsp;boto3<\/h4>\n\n\n\n<p>I will use a Python virtual environment and install boto3 with that<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python3 -m venv .venv<br><br># activate the virtual environemnts on my mac. There is a different commnad<br>#  windows. <br>source .venv\/bin\/activate<\/code><\/pre>\n\n\n\n<p>Install boto3 with pip<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pip install boto3<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Now that we have the installation done, let&#8217;s write the code step by&nbsp;step.<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">1. List the&nbsp;users<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>import boto3<br>def get_users():<br>    iam_client = boto3.client('iam')<br>    response = iam_client.list_users()<br>    return &#91;user&#91;'UserName'] for user in response&#91;'Users']]<br><br>print(get_users())<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*jWF9uFp36BvAHCkPHSNBBQ.png\" alt=\"\"\/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">2. Get the access keys&nbsp;details<\/h4>\n\n\n\n<p>I will use the datetime module to calculate the age from the created date parameter of the access key<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def get_access_keys_age(username):<br>    iam_client = boto3.client('iam')<br>    response = iam_client.list_access_keys(UserName=username).get('AccessKeyMetadata', &#91;])<br>    <br>    access_keys_info = &#91;]<br>    for item in response:<br>        if item&#91;'Status'] == 'Active':<br>            access_key_id = item&#91;'AccessKeyId']<br>            create_date = item&#91;'CreateDate'].date()<br>            age = (date.today() - create_date).days<br>            access_keys_info.append((access_key_id, age))<br>    <br>    return access_keys_info<br><br>print(get_access_keys_age('cliuser-akhilesh'))<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*3Zy2FCbai0P2SXGoWgorBw.png\" alt=\"\"\/><\/figure>\n\n\n\n<p>As you can see, the user <code>cliuser-akhilesh<\/code> has 2 access keys, one with 22 days&#8217; age and the other with 2 days.<\/p>\n\n\n\n<p>For this use case, I will set the access key expiry age as 20 days, and I would want to send email from 15 days (I would want to have 5 days as a buffer to ensure people responsible for rotating email get enough time to address this)<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">3. Check if the expired&nbsp;key<\/h4>\n\n\n\n<p>I want a function that returns an HTML message if the keys are expired. I would include a dynamic link of the AWS IAM user for which the keys have expired.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>username = \"cliuser-akhilesh\" <br>Expiry_days = 20<br>reminder_email_age = Expiry_days - 5<br><br>def if_key_expired(access_key_id, age, reminder_email_age):<br>    if age &gt;= reminder_email_age:<br>        return f'''<br>    &lt;p&gt;Reminder: Access key &lt;strong&gt;{access_key_id}&lt;\/strong&gt; is &lt;strong&gt;{age}&lt;\/strong&gt; days old. Please rotate it.&lt;\/p&gt;<br>    &lt;p&gt;For more details, visit the &lt;a href=\"https:\/\/us-east-1.console.aws.amazon.com\/iam\/home?region=us-east-1#\/users\/details\/{username}?section=security_credentials\"&gt; Rotate this key here&lt;\/a&gt;.&lt;\/p&gt;<br>    '''<br>    return None<br><br>print(if_key_expired(\"AKIA4ZPZU3T7QIPRKR5X\", 22, reminder_email_age))<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*AW-gT002sNT7WU1mlzZuMw.png\" alt=\"\"\/><\/figure>\n\n\n\n<p>That HTML will look like this<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*p8U3qNvLw_vdn3Zak2QUjg.png\" alt=\"\"\/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">4. Process&nbsp;users<\/h4>\n\n\n\n<p>This will return the email body, only for users with access keys about to expire. We will only build an email for these <code>users\/access_keys<\/code> and send an email<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def process_users():<br>    email_body_list = &#91;]<br>    users = get_users()<br>    for user in users:<br>        access_keys_info = get_access_keys_age(user)<br>        for keys in access_keys_info:<br>            access_key_id, age = keys   <br>            email_body = if_key_expired(access_key_id, age, reminder_email_age)<br>            if email_body:<br>                email_body_list.append(email_body)       <br>    return email_body_list<br>            <br>print(type(process_users()))<br>print(process_users())<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*jF5oXpeaWnHHe8NhKBARPQ.png\" alt=\"\"\/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">5. Build an email with the Python email&nbsp;library<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>from email.mime.multipart import MIMEMultipart<br>from email.mime.text import MIMEText<br><br>def build_email_message(to_email, from_email, subject, body):<br>    msg = MIMEMultipart()<br>    msg&#91;'From'] = from_email<br>    msg&#91;'To'] = to_email<br>    msg&#91;'Subject'] = subject<br><br>    body_part = MIMEText(body, 'html')<br>    msg.attach(body_part)<br><br>    return msg<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">6. Send an email using the AWS SES&nbsp;service<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code># Create ses clinet<br>ses_client = boto3.client('ses')<br><br>def send_email(msg, to_emails):<br>    response = ses_client.send_raw_email(<br>        Source=msg&#91;\"From\"],<br>        Destinations=to_emails,<br>        RawMessage={\"Data\": msg.as_string()},<br>    )<br>    return response.get('MessageId', None)<\/code><\/pre>\n\n\n\n<script async src=\"https:\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-2356534591331561\"\n     crossorigin=\"anonymous\"><\/script>\n<ins class=\"adsbygoogle\"\n     style=\"display:block; text-align:center;\"\n     data-ad-layout=\"in-article\"\n     data-ad-format=\"fluid\"\n     data-ad-client=\"ca-pub-2356534591331561\"\n     data-ad-slot=\"3134096072\"><\/ins>\n<script>\n     (adsbygoogle = window.adsbygoogle || []).push({});\n<\/script>\n\n\n\n<h4 class=\"wp-block-heading\">7. Now let\u2019s put it all&nbsp;together<\/h4>\n\n\n\n<p>This function will find all the users with expiring access keys and send an email. I used my emails for this demo; in real scenarios, you can send an email to the whole team.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def main():<br>    subject = f\"AWS Access Key Rotation Reminder -user {username}\"<br>    to_email = \"akhileshmishratoemail@gmail.com\"<br>    from_email = \"akhileshmishrafromemail@gmail.com\"<br>    for email_body in process_users():<br>        email_msg = build_email_message(to_email, from_email, subject, email_body)<br>        email_sent =send_email(email_msg, &#91;to_email])<br>        print(f\"Email sent with Message ID: {email_sent}\")<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*uzdg-rWBSxLzAJl77FFijA.png\" alt=\"\"\/><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*RoHFdXvonABccrM2xeE9pg.png\" alt=\"\"\/><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>Now that we have tested the code on the local machine, we are ready to deploy it on Lambda.<\/strong><\/p>\n\n\n\n<p>Only one part will change on Lambda, the main function will look something like this.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def main(event, context):\n    subject = f\"AWS Access Key Rotation Reminder -user {username}\"\n    to_email = \"aditiyamishranit@gmail.com\"\n    from_email = \"akhileshmishra121990@gmail.com\"\n    for email_body in process_users():\n        email_msg = build_email_message(to_email, from_email,      subject, email_body)\n        email_sent =send_email(email_msg, &#91;to_email])\n        print(f\"Email sent with Message ID: {email_sent}\")<\/code><\/pre>\n\n\n\n<p>The The <code>main<\/code> function will be the entry function for lambda.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AWS SES&nbsp;Part<\/h3>\n\n\n\n<p>You need to validate the identities in AWS before you can send an email to them. Since you will be using the SES sandbox account, you need to validate to_email and from_email.<\/p>\n\n\n\n<p>Go to <code><a href=\"https:\/\/ap-south-1.console.aws.amazon.com\/ses\/home?region=ap-south-1#\/homepage\" rel=\"noreferrer noopener\" target=\"_blank\">Amazon SES<\/a> &gt; <strong>Configuration: Identities<\/strong><\/code><\/p>\n\n\n\n<p>Create and validate identities<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*3UVSnuZASJukfuqdziupKg.png\" alt=\"\"\/><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<script async src=\"https:\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-2356534591331561\"\n     crossorigin=\"anonymous\"><\/script>\n<ins class=\"adsbygoogle\"\n     style=\"display:block; text-align:center;\"\n     data-ad-layout=\"in-article\"\n     data-ad-format=\"fluid\"\n     data-ad-client=\"ca-pub-2356534591331561\"\n     data-ad-slot=\"3134096072\"><\/ins>\n<script>\n     (adsbygoogle = window.adsbygoogle || []).push({});\n<\/script>\n\n\n\n<h3 class=\"wp-block-heading\">Terraform implementation<\/h3>\n\n\n\n<p>I will follow the steps below to write a Terraform function to deploy the Lambda function.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Lambda will expect zipped code, so I will be archiving the code in a zip format using the Terraform <code>archive_file<\/code> data source<\/li>\n\n\n\n<li>Lambda will need access to AWS IAM to get the access key details, so I will be creating an IAM role with an IAM policy with access to list users, get access key data, and send email<\/li>\n\n\n\n<li>Create the Lambda function<\/li>\n\n\n\n<li>Create a cron job that will run this Lambda daily<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*Y8E2cCGS_tHaGIo4BvdT-g.png\" alt=\"\"\/><\/figure>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Archiving the code<\/strong><\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code># zip the code<br>data \"archive_file\" \"lambda_zip\" {<br>  type        = \"zip\"<br>  source_dir  = \"${path.module}\/lambda\/iam-key-rotation\"<br>  output_path = \"${path.module}\/iam-key-rotation.zip\"<br>} <\/code><\/pre>\n\n\n\n<p><code>path.module<\/code> reference to the path of the Terraform config. We use it to write the relative path for the code files.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>IAM role for the Lambda<\/strong><\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code># iam role<br>resource \"aws_iam_role\" \"lambda_role\" {<br>  name = \"iam-key-rotation-role\"<br>  <br>  assume_role_policy = jsonencode({<br>    Version = \"2012-10-17\"<br>    Statement = &#91;<br>      {<br>        Action = \"sts:AssumeRole\"<br>        Effect = \"Allow\"<br>        Principal = {<br>          Service = \"lambda.amazonaws.com\"<br>        }<br>      }<br>    ]<br>  })    <br><br>}<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>IAM policy for the Lambda<\/strong><\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>resource \"aws_iam_policy\" \"lambda_policy\" {<br>  name        = \"iam-key-rotation-policy\"<br>  description = \"Policy for Lambda function to rotate IAM keys\"<br>  <br>  policy = jsonencode({<br>    Version = \"2012-10-17\"<br>    Statement = &#91;<br>      {<br>        Action = &#91;<br>          \"iam:ListAccessKeys\",<br>          \"iam:ListUsers\",<br>        ]<br>        Effect   = \"Allow\"<br>        Resource = \"*\"<br>      },<br>      {<br>        Action = &#91;<br>          \"ses:SendEmail\",<br>          \"ses:SendRawEmail\",<br>        ]<br>        Effect   = \"Allow\"<br>        Resource = \"*\"<br>      }<br>    ]<br>  })<br>}<\/code><\/pre>\n\n\n\n<script async src=\"https:\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-2356534591331561\"\n     crossorigin=\"anonymous\"><\/script>\n<ins class=\"adsbygoogle\"\n     style=\"display:block; text-align:center;\"\n     data-ad-layout=\"in-article\"\n     data-ad-format=\"fluid\"\n     data-ad-client=\"ca-pub-2356534591331561\"\n     data-ad-slot=\"3134096072\"><\/ins>\n<script>\n     (adsbygoogle = window.adsbygoogle || []).push({});\n<\/script>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Attach the policy to the role<\/strong><\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code># # attach policy to role<br>resource \"aws_iam_role_policy_attachment\" \"lambda_policy_attachment\" {<br>  role       = aws_iam_role.lambda_role.name<br>  policy_arn = aws_iam_policy.lambda_policy.arn<br>}<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Lambda function<\/strong><\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code># lambda <br>resource \"aws_lambda_function\" \"my_lambda_function\" {<br>    function_name    = \"iam-key-rotation\"<br>    role             = aws_iam_role.lambda_role.arn<br>    handler          = \"main.main\"<br>    runtime          = \"python3.13\"<br>    timeout          = 60<br>    memory_size      = 128<br><br>    # Use the Archive data source to zip the code<br>    filename         = data.archive_file.lambda_zip.output_path<br>    source_code_hash = data.archive_file.lambda_zip.output_base64sha256<br>}<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>source_code_hash<\/code> will enable Lambda to update the Lambda code whenever a change happens in the Python code.<\/li>\n\n\n\n<li><code>timeout<\/code> is set to 60 seconds, if not set, will fall back to the default 3 seconds<\/li>\n\n\n\n<li><code>handler<\/code> Use the format python_code.python_function. In this use case, main.py is the code that runs when Lambda is invoked, and main() is the entry-level function. Hence, <code>handler = main.main<\/code><\/li>\n<\/ul>\n\n\n\n<p>To enable the cron job trigger, we use an AWS eventbridge rule.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>resource \"aws_cloudwatch_event_rule\" \"cron_lambdas\" {<br>  name                = \"cronjob\"<br>  description         = \"to triggr lambda daily 7.15 pm ist\"<br>  schedule_expression = \"cron(40 13 * * ? *)\"<br>}<br>resource \"aws_cloudwatch_event_target\" \"cron_lambdas\" {<br>  rule = aws_cloudwatch_event_rule.cron_lambdas.name<br>  arn  = aws_lambda_function.my_lambda_function.arn<br>}<\/code><\/pre>\n\n\n\n<p>Also, this cron will need permissions to invoke the lambda on schedule<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Invoke lambda permission<br>resource \"aws_lambda_permission\" \"cron_lambdas\" {<br>  statement_id  = \"key-rotation-lambda-allow\"<br>  action        = \"lambda:InvokeFunction\"<br>  principal     = \"events.amazonaws.com\"<br>  function_name = aws_lambda_function.my_lambda_function.arn<br>  source_arn    = aws_cloudwatch_event_rule.cron_lambdas.arn<br>}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Lambda code is set. I will use the Terraform provider AWS and a remote state file<\/p>\n\n\n\n<p><code>providers.tf<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {<br>  required_version = \"1.8.1\"<br><br>  required_providers {<br>    aws = {<br>      source  = \"hashicorp\/aws\"<br>      version = \"&gt;= 5.32.0\"<br>    }<br>  }<br>}<br><br>provider \"aws\" {<br>  region = \"ap-south-1\"<br>}<br><br># remote backend<br>terraform {<br>  backend \"s3\" {<br>    bucket         = \"state-bucket-879381234673\"<br>    key            = \"lambda-blog\/terraform.tfstate\"<br>    region         = \"ap-south-1\"<br>    encrypt        = true<br>  }<br>}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Now we can deploy the lambda&nbsp;function<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">terraform init<br>terraform plan<br>terraform apply<\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*DAVFHa12ZEPN1tD84xVY8A.png\" alt=\"\"\/><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*G1sTignJR_jTSOBy7rUfnQ.png\" alt=\"\"\/><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<script async src=\"https:\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-2356534591331561\"\n     crossorigin=\"anonymous\"><\/script>\n<ins class=\"adsbygoogle\"\n     style=\"display:block; text-align:center;\"\n     data-ad-layout=\"in-article\"\n     data-ad-format=\"fluid\"\n     data-ad-client=\"ca-pub-2356534591331561\"\n     data-ad-slot=\"3134096072\"><\/ins>\n<script>\n     (adsbygoogle = window.adsbygoogle || []).push({});\n<\/script>\n\n\n\n<h3 class=\"wp-block-heading\">Better version of Python and Lambda&nbsp;code<\/h3>\n\n\n\n<p>To ensure the automation is flexible and maintainable, I\u2019ll implement a configuration-driven approach using environment variables.<\/p>\n\n\n\n<p>This eliminates hardcoded values and allows dynamic configuration management through Terraform, making the solution easily adaptable across different environments and organizations.<\/p>\n\n\n\n<p>Here is the final version of the code.<\/p>\n\n\n\n<p><code>main.py<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import boto3<br>from datetime import date<br>from email.mime.multipart import MIMEMultipart<br>from email.mime.text import MIMEText<br>import os<br><br>from_email = os.environ.get('FROM_EMAIL')<br>to_email = os.environ.get('TO_EMAIL')<br>Expiry_days = int(os.environ.get('EXPIRY_DAYS', 90))  # Default to 90 days if not set<br>reminder_email_age = Expiry_days - 5<br><br>def get_users():<br>    iam_client = boto3.client('iam')<br>    response = iam_client.list_users()<br>    return &#91;user&#91;'UserName'] for user in response&#91;'Users']]<br><br>def get_access_keys_age(username):<br>    iam_client = boto3.client('iam')<br>    response = iam_client.list_access_keys(UserName=username).get('AccessKeyMetadata', &#91;])<br>    <br>    access_keys_info = &#91;]<br>    for item in response:<br>        if item&#91;'Status'] == 'Active':<br>            access_key_id = item&#91;'AccessKeyId']<br>            create_date = item&#91;'CreateDate'].date()<br>            age = (date.today() - create_date).days<br>            access_keys_info.append((access_key_id, age))<br>    <br>    return access_keys_info<br><br>def if_key_expired(username, access_key_id, age, reminder_email_age):<br>    if age &gt;= reminder_email_age:<br>        return f'''<br>    &lt;p&gt;Reminder: Access key &lt;strong&gt;{access_key_id}&lt;\/strong&gt; is &lt;strong&gt;{age}&lt;\/strong&gt; days old. Please rotate it.&lt;\/p&gt;<br>    &lt;p&gt;For more details, visit the &lt;a href=\"https:\/\/us-east-1.console.aws.amazon.com\/iam\/home?region=us-east-1#\/users\/details\/{username}?section=security_credentials\"&gt; Rotate this key here&lt;\/a&gt;.&lt;\/p&gt;<br>    '''<br>    return None<br><br>def process_users():<br>    email_body_list = &#91;]<br>    users = get_users()<br>    for user in users:<br>        access_keys_info = get_access_keys_age(user)<br>        for keys in access_keys_info:<br>            access_key_id, age = keys   <br>            email_body = if_key_expired(user, access_key_id, age, reminder_email_age)<br>            if email_body:<br>                email_body_list.append(email_body)       <br>    return email_body_list<br>            <br>def build_email_message(to_email, from_email, subject, body):<br>    msg = MIMEMultipart()<br>    msg&#91;'From'] = from_email<br>    msg&#91;'To'] = to_email<br>    msg&#91;'Subject'] = subject<br><br>    body_part = MIMEText(body, 'html')<br>    msg.attach(body_part)<br>    return msg<br><br>def send_email(msg, to_emails):<br>    ses_client = boto3.client('ses')<br>    response = ses_client.send_raw_email(<br>        Source=msg&#91;\"From\"],<br>        Destinations=to_emails,<br>        RawMessage={\"Data\": msg.as_string()},<br>    )<br>    return response.get('MessageId', None)<br><br>def main(event, context):<br>    subject = f\"AWS Access Key Rotation Reminder\"<br>    for email_body in process_users():<br>        email_msg = build_email_message(to_email, from_email, subject, email_body)<br>        email_sent =send_email(email_msg, &#91;to_email])<br>        print(f\"Email sent with Message ID: {email_sent}\")<\/code><\/pre>\n\n\n\n<p>And updated the Terraform resource for Lambda<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># lambda <br>resource \"aws_lambda_function\" \"my_lambda_function\" {<br>    function_name    = \"iam-key-rotation\"<br>    role             = aws_iam_role.lambda_role.arn<br>    handler          = \"lambda_function.lambda_handler\"<br>    runtime          = \"python3.13\"<br>    timeout          = 60<br>    memory_size      = 128<br><br>    # Use the Archive data source to zip the code<br>    filename         = data.archive_file.lambda_zip.output_path<br>    source_code_hash = data.archive_file.lambda_zip.output_base64sha256<br>    environment {<br>      variables = {<br>        \"to_email\" = \"akhileshmishratoemail@gmail.com\"<br>        \"from_email\" = \"akhileshmishra1from@gmail.com\"<br>        \"Expiry_days\" = 20<br>      }<br>    }   <\/code><\/pre>\n\n\n\n<p>\u2705 <a href=\"https:\/\/github.com\/akhileshmishrabiz\/Devops-zero-to-hero\/tree\/main\/AWS-Projects\/lambda-iam-key-rotation\" rel=\"noreferrer noopener\" target=\"_blank\"><strong>You can find the entire code for this blog post on my public GitHub repo<\/strong><\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>This is all for this blog post. Let me know your thoughts in the comments.<\/p>\n\n\n\n<p>If you liked this blog post, then you will love this one too<\/p>\n\n\n\n<p>\ud83d\udc49 <strong><a href=\"https:\/\/livingdevops.com\/devops\/terraform-to-deploy-aws-lambda-function-with-s3-trigger\/\" target=\"_blank\" rel=\"noreferrer noopener\">Terraform To Deploy AWS Lambda Function With S3 Trigger<\/a><\/strong><\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Building a production Lambda function that monitors IAM access keys and sends automated email alerts using boto3 and AWS&nbsp;SES. Scenario In my project, we have over 10+ users with access keys. Most of the access keys were 300\u2013400 days old. Having access keys lying around for a long period poses a security risk, which is [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,10,12],"tags":[],"class_list":["post-875","post","type-post","status-publish","format-standard","hentry","category-devops","category-aws","category-tutorials"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/posts\/875","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/comments?post=875"}],"version-history":[{"count":3,"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/posts\/875\/revisions"}],"predecessor-version":[{"id":879,"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/posts\/875\/revisions\/879"}],"wp:attachment":[{"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/media?parent=875"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/categories?post=875"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/livingdevops.com\/wp-json\/wp\/v2\/tags?post=875"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}