<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Jenna's Runbooks]]></title><description><![CDATA[Documenting cloud operations, developer tooling, and AI infrastructure. Content is organized into four main sections: Platform & AI Infrastructure (runtimes, K8]]></description><link>https://blog.jennasrunbooks.com</link><generator>RSS for Node</generator><lastBuildDate>Sat, 05 Sep 2026 11:33:45 GMT</lastBuildDate><atom:link href="https://blog.jennasrunbooks.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[AWS IAM User Management | Terraform & Boto3 Scripts]]></title><description><![CDATA[Overview
Finding the balance between ensuring the security of user identities while providing a self-service user experience takes continuous effort. With the constantly evolving cloud landscape and t]]></description><link>https://blog.jennasrunbooks.com/aws-iam-user-management-terraform-boto3-scripts</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/aws-iam-user-management-terraform-boto3-scripts</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[Python]]></category><category><![CDATA[Security]]></category><category><![CDATA[Cloud]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Wed, 20 Sep 2023 15:54:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/31-pOduwZGE/upload/48495d3e12c02a1b5847060bdf6c7163.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Overview</h2>
<p>Finding the balance between ensuring the security of user identities while providing a self-service user experience takes continuous effort. With the constantly evolving cloud landscape and the security around it, IAM user administration can become a burdensome task. In this post, I'll provide the steps to an approach that attempts to ensure the security of AWS IAM users while streamlining administrative overhead and maintaining a self-service user experience by using a combination of Terraform code and Python Boto3 scripts.</p>
<h2>Code Functionality</h2>
<p>The solution below is comprised of both Terraform code and Python boto3 scripts creating an efficient method for managing IAM users and their associated login profiles, MFA devices and access keys.</p>
<h3>Terraform Code</h3>
<ul>
<li><p>Manages IAM user identities, email address tags and optional custom tags (i.e. Access Key descriptions)</p>
</li>
<li><p><code>aws_iam_user</code> resource block is used with a for_each meta-argument which loops through the map of users assigned to the <code>user_info</code> variable</p>
</li>
<li><p><code>local-exec</code> provisioner is used to call external executable scripts which are uniquely defined based on the creation and destruction of an IAM user</p>
</li>
<li><p><code>create-time</code> provisioner creates the login profile and generates a temporary login password during user creation</p>
</li>
<li><p><code>destroy-time</code> provisioner destroys the login profile, MFA devices and access keys during user destruction</p>
</li>
</ul>
<p><a href="https://github.com/jksprattler/aws-security/blob/main/terraform/iam/users.tf"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695215428174/b7a2cdcb-9676-415f-9901-dc856315311f.png" alt="" style="display:block;margin:0 auto" /></a></p>
<h3>Python Boto3 Scripts</h3>
<ul>
<li><p>Automatically invoked by the <code>local-exec</code> provisioner upon <code>terraform apply</code> and are unique based on the definition set in the <code>when</code> meta-argument</p>
</li>
<li><p>These scripts allow users to manage their MFA devices and access keys rather than having these resources checked into the terraform state and managed by an IAM admin</p>
</li>
<li><p>Allows IAM admins to run a single command to create or destroy a user rather than running additional commands to invoke scripts to create or destroy user resources such as the login profile, MFA devices and access keys</p>
</li>
</ul>
<h3>Code Resources</h3>
<p><em>Link to the Terraform code:</em> <a href="https://github.com/jksprattler/aws-security/blob/main/terraform/iam/users.tf">https://github.com/jksprattler/aws-security/blob/main/terraform/iam/users.tf</a></p>
<p><em>Link to the local-exec provisioner boto3 script that</em> <em><strong>creates</strong></em> <em>the login profile and generates the temp password for the new user:</em> <a href="https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_user_password_reset.py">https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_user_password_reset.py</a></p>
<p><em>Link to the local-exec provisioner boto3 script that</em> <em><strong>destroys</strong></em> <em>the login profile, MFA devices and access keys for a removed user:</em> <a href="https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_user_cleanup.py">https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_user_cleanup.py</a></p>
<p><em>Link to my blog post on IAM User Password Expiry Notice which is why I have the email tags required in this post on IAM user management:</em> <a href="https://blog.jennasrunbooks.com/aws-lambda-function-iam-user-password-expiry-notice-ses-boto3-terraform">https://blog.jennasrunbooks.com/aws-lambda-function-iam-user-password-expiry-notice-ses-boto3-terraform</a></p>
<h2>Usage</h2>
<h3>Create IAM User</h3>
<p>Creating a new AWS IAM user with the provided terraform configuration requires updating the <code>user_info</code> variable definition by adding a new map object assignment containing the new username and the required corresponding <code>email</code> address key-value tag. The optional <code>user_tags</code> can be disregarded for now - these are only needed if the user decides to apply a description to their access key ID. The need to update these optional tags would be indicated by a future terraform plan output where the key-value tags are missing from the terraform state.</p>
<p>Below is some sample terraform output from creating a new IAM user called "userB":</p>
<pre><code class="language-yaml">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_iam_user.this["userB"] will be created
  + resource "aws_iam_user" "this" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "userB"
      + path          = "/"
      + tags          = {
          + "email"            = "userB@jennasrunbooks.com"
          + "environment-type" = "lab"
          + "provisioner"      = "terraform"
          + "repo"             = "aws-security"
          + "resource-owner"   = "aws-landing-zone@jennasrunbooks.com"
        }
      + tags_all      = {
          + "email"            = "userB@jennasrunbooks.com"
          + "environment-type" = "lab"
          + "provisioner"      = "terraform"
          + "repo"             = "aws-security"
          + "resource-owner"   = "aws-landing-zone@jennasrunbooks.com"
        }
      + unique_id     = (known after apply)
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_iam_user.this["userB"]: Creating...
aws_iam_user.this["userB"]: Provisioning with 'local-exec'...
aws_iam_user.this["userB"] (local-exec): Executing: ["/bin/sh" "-c" "python ../../scripts/aws/aws_iam_user_password_reset.py profile -u userB"]
aws_iam_user.this["userB"] (local-exec): New login profile has been created for: userB
aws_iam_user.this["userB"] (local-exec): Login with temp password:
aws_iam_user.this["userB"] (local-exec): ,)y3}"fXafTHj=Acei';
aws_iam_user.this["userB"] (local-exec): Password reset will be enforced upon initial login
aws_iam_user.this["userB"]: Creation complete after 1s [id=userB]
Releasing state lock. This may take a few moments...

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
</code></pre>
<p>As seen in the above output, the <code>local-exec</code> provisioner provides the temporary password to the login profile for the new user based on this line:</p>
<p><code>aws_iam_user.this["userB"] (local-exec): ,)y3}"fXafTHj=Acei';</code></p>
<p>This can now be directly provided to the new user so they can log into the AWS Console UI where they'll be forced to reset their password and then proceed to set up their MFA devices and Access keys on their own.</p>
<h3>Destroy IAM User</h3>
<p>Destroying an AWS IAM user is as simple as deleting their map object assignment values from the <code>user_info</code> variable. Additionally, you'd want to remove the user from any IAM groups however, this post is focused on IAM user management using the <code>aws_iam_user</code> terraform resource block.</p>
<p>Below is some sample terraform output from destroying the "userB" IAM user created in the previous section:</p>
<pre><code class="language-yaml">Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_iam_user.this["userB"] will be destroyed
  # (because key ["userB"] is not in for_each map)
  - resource "aws_iam_user" "this" {
      - arn           = "arn:aws:iam::&lt;accountID&gt;:user/userB" -&gt; null
      - force_destroy = false -&gt; null
      - id            = "userB" -&gt; null
      - name          = "userB" -&gt; null
      - path          = "/" -&gt; null
      - tags          = {
          - "AKIASRJ6UGTM2MP47QO6" = "testkey2"
          - "AKIASRJ6UGTMV3JU6CU2" = "testkey1"
          - "email"                = "userB@jennasrunbooks.com"
          - "environment-type"     = "lab"
          - "provisioner"          = "terraform"
          - "repo"                 = "aws-security"
          - "resource-owner"       = "aws-landing-zone@jennasrunbooks.com"
        } -&gt; null
      - tags_all      = {
          - "AKIASRJ6UGTM2MP47QO6" = "testkey2"
          - "AKIASRJ6UGTMV3JU6CU2" = "testkey1"
          - "email"                = "userB@jennasrunbooks.com"
          - "environment-type"     = "lab"
          - "provisioner"          = "terraform"
          - "repo"                 = "aws-security"
          - "resource-owner"       = "aws-landing-zone@jennasrunbooks.com"
        } -&gt; null
      - unique_id     = "AIDASRJ6UGTMQUHFQ77G4" -&gt; null
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_iam_user.this["userB"]: Destroying... [id=userB]
aws_iam_user.this["userB"]: Provisioning with 'local-exec'...
aws_iam_user.this["userB"] (local-exec): Executing: ["/bin/sh" "-c" "python ../../scripts/aws/aws_iam_user_cleanup.py userB"]
aws_iam_user.this["userB"] (local-exec): Deleting login profile for userB
aws_iam_user.this["userB"] (local-exec): Deleting MFA device for userB: arn:aws:iam::&lt;accountID&gt;:u2f/user/testuser/testfido-I5GXZJQHZL3FXDIXX4
aws_iam_user.this["userB"] (local-exec): Deleted access key: AKIASRJ6UGTMV3JU6CU2 for user: userB
aws_iam_user.this["userB"] (local-exec): Deleted access key: AKIASRJ6UGTM2MP47QO6 for user: userB
aws_iam_user.this["userB"]: Destruction complete after 2s
Releasing state lock. This may take a few moments...

Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
</code></pre>
<div>
<div>💡</div>
<div>The <code>force_destroy</code> option when set to true in the <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user#force_destroy" style="pointer-events:none">aws_iam_user </a>resource block states: <em>When destroying this user, destroy even if it has non-Terraform-managed IAM access keys, login profile or MFA devices. Without force_destroy a user with non-Terraform-managed access keys and login profile will fail to be destroyed. </em>I tried testing this option by destroying a user with an associated login profile, MFA device and access key while running AWS provider version <code>5.16.2</code> and terraform version <code>1.5.7</code> and it failed with the following error:</div>
</div>

<pre><code class="language-yaml">╷
│ Error: deleting IAM User (blahblah): DeleteConflict: Cannot delete entity, must delete login profile first.
│       status code: 409, request id: ca2c4e6e-620a-4082-9077-04af49b29bcb
│
</code></pre>
<h2>Conclusion</h2>
<p>By integrating Terraform and Python Boto3 scripts, the above approach attempts to provide an efficient solution for managing AWS IAM users while maintaining a secure yet self-service environment for the end user. Administrative overhead should be reduced since MFA device and access key management remain in the hands of the user and outside of the terraform state. As long as you have good documentation and your users are savvy enough managing MFA devices and access keys should not be an issue for them. The Terraform code also follows the DRY principle to reduce repetitive lines of code with a single <code>aws_iam_user</code> resource block used.</p>
]]></content:encoded></item><item><title><![CDATA[Detect Your VM's Cloud Provider and Region | Bash Script]]></title><description><![CDATA[Overview
Managing a global fleet of virtual machines (VMs) across various cloud service providers (CSPs) can be a challenging task. Depending on the naming convention applied to your hostnames, quickl]]></description><link>https://blog.jennasrunbooks.com/detect-your-vms-cloud-provider-and-region-bash-script</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/detect-your-vms-cloud-provider-and-region-bash-script</guid><category><![CDATA[Bash]]></category><category><![CDATA[Script]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[multicloud]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Wed, 09 Aug 2023 15:13:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/2-HWopOOXP4/upload/30e2d41a8ddfa692c61ac49d5c0c7f9a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Overview</h2>
<p>Managing a global fleet of virtual machines (VMs) across various cloud service providers (CSPs) can be a challenging task. Depending on the naming convention applied to your hostnames, quickly identifying the CSP and region for a VM could be a challenge. To simplify this process, I've developed a bash script that takes advantage of each supported CSPs' available Instance Metadata Service (IMDS) to effectively identify the CSP and region, making troubleshooting more efficient.</p>
<p>Shout out to my colleague, <a href="https://hashnode.com/@aaronfuj" class="user-mention" data-type="mention" title="Aaron Fujimoto">Aaron Fujimoto</a> who gave me the idea and an initial framework for this as discussed in his blog post <a href="https://aaronfujimoto.hashnode.dev/identifying-your-cloud-provider-within-a-virtual-machine">here</a>!</p>
<h2>Purpose</h2>
<p>At <a href="https://www.kentik.com/product/global-agents/">Kentik</a>, we manage hundreds of global agents hosted across more than a dozen different CSPs for our Synthetics product. The hostnames assigned to these VMs follow a naming convention that identifies the CSP using an assigned numerical value and region using an IATA code. While the intent for this was to provide standardization across all environments, it's difficult to speedily identify what CSP and/or region you might be dealing with upon receiving an alert for a specific host. There are methods for identifying this information such as searching Kibana logs or matching up the CSP numerical identifier and IATA code for the region which has been my use case. To streamline this identification process, I've developed this simple tool supporting 9 different CSPs, including Azure, GCP, AWS, Alibaba Cloud, Vultr/Choopa, DigitalOcean, Tencent Cloud, Exoscale, and IBM Cloud.</p>
<h2>Script Functionality</h2>
<p>This script utilizes a simple yet effective combination of Linux tools including <code>curl</code>, <code>jq</code>, and <code>grep</code>. By extracting the CSP and region details, the script enables faster navigation within the CSPs' portal and any other internal applications or tools you might use allowing you to pinpoint the affected VM within the appropriate region. For example, when troubleshooting an issue on an AWS host you need to know the region where it's hosted so you can navigate to the appropriate region's page within the AWS portal.</p>
<p><a href="https://github.com/jksprattler/multi-cloud-tools/blob/main/scripts/get-cloud-provider-region.sh"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1691593471740/c8230454-fcc3-436e-a9cf-2aa399a66724.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p><em>Link to the script:</em> <a href="https://github.com/jksprattler/multi-cloud-tools/blob/main/scripts/get-cloud-provider-region.sh">https://github.com/jksprattler/multi-cloud-tools/blob/main/scripts/get-cloud-provider-region.sh</a></p>
<h2>Usage</h2>
<p>Using the script is straightforward and is intended to be run by the root user after being loaded into the <code>/usr/local/sbin</code> directory on all cloud hosts across an infrastructure. Once executed, it cycles through a list of if/else statements until it matches the conditional for the CSPs' metadata the VM is running on. Next, it identifies the region based on the matching CSP statement.</p>
<p>Below are some sample test results from running the script across the 9 different supported CSPs in my environment. You can see how these example hostnames make it difficult to identify the CSP/region:</p>
<pre><code class="language-bash">root@dev-2-mxp:~# get-cloud-provider-region.sh
CSP: aws
Region: eu-south-1
root@dev-12-cdg:~# get-cloud-provider-region.sh
CSP: azure
Region: FranceCentral
root@dev-5-ams:~# get-cloud-provider-region.sh
CSP: digitalocean
Region: ams3
root@dev-9-hkg:~# get-cloud-provider-region.sh
CSP: alibabacloud
Region: cn-hongkong
root@dev-1-ams:~# get-cloud-provider-region.sh
CSP: gcp
Region: europe-west4-b
root@dev-4-atl:~# get-cloud-provider-region.sh
CSP: vultr/choopa
Region: ATL US
root@dev-10-bkk:~# get-cloud-provider-region.sh
CSP: tencentcloud
Region: ap-bangkok
root@dev-11-fra:~# get-cloud-provider-region.sh
CSP: exoscale
Region: de-fra-1
root@dev-7-dfw:~# get-cloud-provider-region.sh
CSP: ibmcloud
Region: us-south-1
</code></pre>
<h2>Conclusion</h2>
<p>Managing VMs across various CSPs and regions is now much more efficient using this script. By combining <code>curl</code>, <code>jq</code>, and <code>grep</code> Linux tools, this solution leads to faster troubleshooting. The ability to quickly identify the CSP and region of a VM, right from within the host, showcases the potential of leveraging simple tools like this for enhanced operational efficiency.</p>
]]></content:encoded></item><item><title><![CDATA[AWS S3 Object Finder | Boto3 Script]]></title><description><![CDATA[Overview
🔎 Quickly find AWS S3 Objects inside buckets hosting huge volumes of files using my latest Boto3 script! You can easily locate specific objects in your AWS profile by providing a few command]]></description><link>https://blog.jennasrunbooks.com/aws-s3-object-finder-boto3-script</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/aws-s3-object-finder-boto3-script</guid><category><![CDATA[AWS]]></category><category><![CDATA[Python]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[S3]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Mon, 12 Jun 2023 13:55:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/VSwlS0PpWwc/upload/3a629a851e00bfb51bc9a5944aa5c5f6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Overview</h2>
<p>🔎 Quickly find AWS S3 Objects inside buckets hosting huge volumes of files using my latest Boto3 script! You can easily locate specific objects in your AWS profile by providing a few command line arguments. By specifying the AWS profile, bucket name, object name, and optional prefix you can efficiently search for objects and obtain a list of matching keys, allowing you to pinpoint the location of a particular file.</p>
<p>This 📜 performs the following tasks:</p>
<p>✔️ Handle pagination when dealing with large buckets containing thousands of objects</p>
<p>✔️ Perform a recursive search for an object in an S3 bucket, considering the object's name and optional prefix</p>
<p>✔️ Display the keys of matching objects found in the bucket</p>
<p>✔️ List all buckets in a specified AWS profile</p>
<p>✔️ List top-level prefixes in a specified bucket</p>
<h2>Purpose</h2>
<p>This AWS S3 Object Finder script is particularly helpful when troubleshooting upload error logs especially when dealing with buckets that host thousands of objects and extensive prefixes. In these scenarios, manually searching for objects can be very time-consuming. By utilizing the script's pagination and object prefix features, you can significantly speed up the search process and narrow down the results to the desired subset of objects. This saves both time and effort allowing you to efficiently troubleshoot specific object data without having to search for objects manually.</p>
<h2>Script Functionality</h2>
<p><a href="https://github.com/jksprattler/aws-tools/blob/main/scripts/aws_s3_object_finder.py"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686577136865/8d89c342-b5a9-48ea-b798-473987045365.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p><em>Link to the script:</em> <a href="https://github.com/jksprattler/aws-tools/blob/main/scripts/aws_s3_object_finder.py">https://github.com/jksprattler/aws-tools/blob/main/scripts/aws_s3_object_finder.py</a></p>
<h3>Usage</h3>
<p>Here's a look at the options available from the <code>-h/--help</code> output:</p>
<pre><code class="language-bash">❯ python aws_s3_object_finder.py --help
usage: aws_s3_object_finder.py [-h] -p PROFILE [-b BUCKET] [-o OBJECT] [-x PREFIX] {list-buckets,list-prefixes} ...

Search for an object in an S3 bucket.

positional arguments:
  {list-buckets,list-prefixes}
                        Additional commands
    list-buckets        List buckets in profile, use after -p
    list-prefixes       List top-level prefixes in bucket, use after -p and -b

optional arguments:
  -h, --help            show this help message and exit
  -p PROFILE, --profile PROFILE
                        AWS profile name
  -b BUCKET, --bucket BUCKET
                        S3 bucket name
  -o OBJECT, --object OBJECT
                        Object name to search for
  -x PREFIX, --prefix PREFIX
                        Object prefix for high volume bucket search
</code></pre>
<p>Here's an example of listing all buckets in a specified AWS profile:</p>
<pre><code class="language-bash">❯ python aws_s3_object_finder.py -p cloud-sandbox list
sandbox-cloudtrail
sandbox-trail-bucket
lstest2022
</code></pre>
<p>Here's an example of listing all top-level prefixes in a specified bucket:</p>
<pre><code class="language-bash">❯ python aws_s3_object_finder.py -p default -b jennasprattler.com list-prefixes
assets/
images/
</code></pre>
<p>Here's an example of listing all S3 objects in a specified bucket by calling a keyword in the prefix as the object:</p>
<pre><code class="language-bash">❯ python aws_s3_object_finder.py -p cloud-sandbox -b sandbox-cloudtrail -o us-west-2
'us-west-2' found in bucket 'sandbox-cloudtrail'
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_1bxLMBY0uRL1X67x.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_HzrxfJgKaRrQr2d3.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_Uyg59kXzzIB0y2YS.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_rzKX1qISHRXL6aru.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2105Z_GlZuUn6pUrCDzbhr.json.gz
### Redacted for brevity
</code></pre>
<p>Here's an example of listing all objects in a bucket under a specific prefix where there are 1,000s of objects requiring pagination. If you don't know the specific file/object name then you can define the closest matching prefix anywhere in the key as the object argument followed by the initial prefix of the key as the prefix argument:</p>
<pre><code class="language-bash">❯ python aws_s3_object_finder.py -p cloud-sandbox -b sandbox-cloudtrail -o us-west-2 -x AWSLogs/
'us-west-2' found in bucket 'sandbox-cloudtrail'
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_1bxLMBY0uRL1X67x.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_HzrxfJgKaRrQr2d3.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_Uyg59kXzzIB0y2YS.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_rzKX1qISHRXL6aru.json.gz
### Redacted for brevity
</code></pre>
<p>In some cases, you will need to be more granular with how you specify objects and prefixes based on your prefix structure. In other words, you could have duplicate or overlapping prefix names causing a search to fail. The solution for this is to specify additional values in your delimiter-separated values for '/' for the prefix argument:</p>
<pre><code class="language-bash">❯ python aws_s3_object_finder.py -p cloud-sandbox -b sandbox-cloudtrail -o us-west-2 -x AWSLogs/accountId/CloudTrail/
'us-west-2' found in bucket 'sandbox-cloudtrail'
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_1bxLMBY0uRL1X67x.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_HzrxfJgKaRrQr2d3.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_Uyg59kXzzIB0y2YS.json.gz
Key(s) matching 'us-west-2': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_rzKX1qISHRXL6aru.json.gz
### Redacted for brevity
</code></pre>
<p>Here's an example of finding a specific object/filename in a bucket hosting thousands of objects by specifying a prefix with the object argument:</p>
<pre><code class="language-bash">❯ python aws_s3_object_finder.py -p cloud-sandbox -b sandbox-cloudtrail -o accountId_CloudTrail_us-west-2_20220120T2100Z_1bxLMBY0uRL1X67x.json.gz -x AWSLogs/
'accountId_CloudTrail_us-west-2_20220120T2100Z_1bxLMBY0uRL1X67x.json.gz' found in bucket 'sandbox-cloudtrail'
Key(s) matching 'accountId_CloudTrail_us-west-2_20220120T2100Z_1bxLMBY0uRL1X67x.json.gz': sandbox-cloudtrail/AWSLogs/accountId/CloudTrail/us-west-2/2022/01/20/accountId_CloudTrail_us-west-2_20220120T2100Z_1bxLMBY0uRL1X67x.json.gz
</code></pre>
<h3>Code Explanation</h3>
<p>Inside the main function, the args are set to the <code>parse_args()</code> function. If no arguments are provided the script outputs the usage from the parser and exits. The next conditional will run the list_buckets function if the <code>list-buckets</code> argument is provided with a profile. Next, it will run the list_prefixes function if the <code>list-prefixes</code> argument is provided with a profile and bucket. Finally, the <code>search_s3_object</code> function is run when at least the profile, bucket and object are provided - the prefix is optional.</p>
<pre><code class="language-python">def main():
    """ Parse args and call either S3 bucket list or object search function """
    args = parse_args()

    if not args:
        return

    if args.command == 'list-buckets':
        list_buckets(args.profile)
    elif args.command == 'list-prefixes':
        list_prefixes(args.profile, args.bucket)
    else:
        search_s3_object(args.profile, args.bucket, args.object, args.prefix)

if __name__ == "__main__":
    main()
</code></pre>
<p>Below is the <code>parse_arguments()</code> function. This is self-explanatory by reviewing the help comments for each of the arguments. Also, see usage in the output example above.</p>
<pre><code class="language-python">def parse_args():
    """ Define cli args to be parsed into main() """
    parser = argparse.ArgumentParser(description='Search for an object in an S3 bucket.')
    parser.add_argument('-p', '--profile', required=True, help='AWS profile name')
    parser.add_argument('-b', '--bucket', help='S3 bucket name')
    parser.add_argument('-o', '--object', help='Object name to search for')
    parser.add_argument('-x', '--prefix', help='Object prefix for high volume bucket search')
    subparser = parser.add_subparsers(dest='command', help='Additional commands')
    subparser.add_parser('list-buckets',
                         help='List buckets in profile, use after -p')
    subparser.add_parser('list-prefixes', help='List top-level prefixes in bucket,\
                                                use after -p and -b')
    args = parser.parse_args()

    if args.command == 'list-buckets':
        return args

    if args.command == 'list-prefixes' and args.bucket:
        return args

    if not args.bucket or (not args.bucket and not args.command == 'list-prefixes') \
        or not args.object:
        parser.print_usage()
        return None

    return args
</code></pre>
<p>If the user provides the <code>list-buckets</code> argument then the <code>list_buckets(profile_name)</code> function is called and provides the output of all buckets in the specified AWS profile. It creates a session using the provided profile name, establishes an S3 client using the session, and sends a request to list the buckets associated with the profile.</p>
<pre><code class="language-python">def list_buckets(profile_name):
    """ List buckets in specified profile """
    session = boto3.Session(profile_name=profile_name)
    s3_client = session.client('s3')
    buckets = s3_client.list_buckets()['Buckets']

    for bucket in buckets:
        print(bucket['Name'])
</code></pre>
<p>If the user provides the <code>list-prefixes</code> argument then the <code>list_prefixes(profile_name, bucket_name)</code> function is called and provides the output of all top-level prefixes within the specified bucket.</p>
<pre><code class="language-python">def list_prefixes(profile_name, bucket_name):
    """ List top-level prefixes in a specified bucket """
    session = boto3.Session(profile_name=profile_name)
    s3_client = session.client('s3')
    paginator = s3_client.get_paginator('list_objects_v2')
    list_objects_args = {'Bucket': bucket_name, 'Delimiter': '/'}
    page_iterator = paginator.paginate(**list_objects_args)

    prefixes = []
    for page in page_iterator:
        if 'CommonPrefixes' in page:
            # Iterate over the CommonPrefixes list and extract top-level prefixes
            prefixes.extend([prefix['Prefix'] for prefix in page['CommonPrefixes']])

    if prefixes:
        for prefix in prefixes:
            print(prefix)
    else:
        print(f"No top-level prefixes found in bucket '{bucket_name}'")
</code></pre>
<p>Excluding any of the "list*" arguments, when the user provides the <code>--object</code> and/or <code>--prefix</code> argument with the AWS profile then the <code>search_s3_object(profile_name, bucket_name, object_name, object_prefix)</code> function is called. It creates a session using the specified profile name, creates an S3 client using the session, and sends a request to list the objects in the specified bucket. The response is then processed, filtering the objects based on the provided object name and object prefix. If any matching objects are found, their keys are printed to the console, indicating their location within the bucket. If no matching objects are found, a message is printed to indicate that the object was not found in the bucket.</p>
<pre><code class="language-python">def search_s3_object(profile_name, bucket_name, object_name, object_prefix):
    """
    Create a session using the specified profile
    Create s3 client using the session
    Perform recursive search using pagination for the S3 bucket object
    Use optional prefix when dealing w/ high volume objects
    """
    session = boto3.Session(profile_name=profile_name)
    s3_client = session.client('s3')
    paginator = s3_client.get_paginator('list_objects_v2')
    list_objects_args = {'Bucket': bucket_name}

    if object_prefix:
        list_objects_ars['Prefix'] = object_prefix

    page_iterator = paginator.paginate(**list_objects_args)

    for page in page_iterator:
        if 'Contents' in page:
            # Check if the object name is in the retrieved keys
            objects = [obj['Key'] for obj in page['Contents'] if object_name in obj['Key']]

            if objects:
                print(f"'{object_name}' found in bucket '{bucket_name}'")
                for obj in objects:
                    print(f"Key(s) matching '{object_name}': {bucket_name}/{obj}")
                return

    print(f"'{object_name}' not found in bucket '{bucket_name}'")
</code></pre>
<p>A few items to highlight from the <code>search_s3_object()</code> function:</p>
<ul>
<li><p><code>list_objects_args = {'Bucket': bucket_name}</code>: This line creates a dictionary called <code>list_objects_args</code> with a key-value pair. The key is <code>'Bucket'</code>, and the value is the <code>bucket_name</code> variable. This dictionary is used to specify the bucket name when calling the <code>list_objects_v2</code> API.</p>
</li>
<li><p><code>if object_prefix: list_objects_args['Prefix'] = object_prefix</code>: This conditional statement checks if the <code>object_prefix</code> variable has a value. If <code>object_prefix</code> is not empty or <code>None</code>, it means a prefix was provided. In that case, it adds another key-value pair to the <code>list_objects_args</code> dictionary. The key is <code>'Prefix'</code>, and the value is the <code>object_prefix</code> variable. This prefix specifies a filter to narrow down the objects returned by the API call.</p>
</li>
<li><p><code>page_iterator = paginator.paginate(**list_objects_args)</code>: This line creates a <code>page_iterator</code> object using the <code>paginator.paginate()</code> method. The double asterisks <code>**</code> before <code>list_objects_args</code> unpacks the dictionary into keyword arguments. This means that the <code>paginator.paginate()</code> method receives the dictionary key-value pairs as separate arguments. In this case, it passes the <code>'Bucket'</code> and <code>'Prefix'</code> arguments to the <code>paginator.paginate()</code> method.</p>
</li>
<li><p>These provide the necessary arguments for the <code>paginator.paginate()</code> method based on the provided bucket name and optional object prefix. It allows you to paginate through the S3 bucket objects, retrieving a subset of objects at a time, based on the provided arguments while still offering flexibility in that the <code>--prefix</code> argument is not required to run the script although it will still be needed if your bucket consists of thousands of objects.</p>
</li>
</ul>
<h2>Conclusion</h2>
<p>This approach to searching for S3 bucket objects provides quick troubleshooting capability and enables you to locate data with ease. Whether you need to identify a precise object location or navigate through large buckets with complex prefixes, this AWS Object Finder script can be a helpful tool when working with AWS S3!</p>
<h2><strong>Boto3 Resources</strong></h2>
<ul>
<li><p>list_objects_v2 - Boto3 1.26.151 documentation: <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/list_objects_v2.html">https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/list_objects_v2.html</a></p>
</li>
<li><p>list_buckets - Boto3 1.26.151 documentation: <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/list_buckets.html">https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/list_buckets.html</a></p>
</li>
<li><p>Paginators - Boto3 1.26.151 documentation: <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/guide/paginators.html">https://boto3.amazonaws.com/v1/documentation/api/latest/guide/paginators.html</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[AWS Kubernetes Cluster Checkouts | Bash Script]]></title><description><![CDATA[Overview
Perform checkouts against your AWS Kubernetes cluster before and after implementing changes such as upgrades by using my latest Bash shell script! I took my AKS checkouts script from a couple]]></description><link>https://blog.jennasrunbooks.com/aws-kubernetes-cluster-checkouts-bash-script</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/aws-kubernetes-cluster-checkouts-bash-script</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Script]]></category><category><![CDATA[kubectl]]></category><category><![CDATA[EKS]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Tue, 06 Jun 2023 16:58:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/eUMEWE-7Ewg/upload/ac7b930f37fd937468f074dd4c9ef6de.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Overview</strong></h2>
<p>Perform checkouts against your AWS Kubernetes cluster before and after implementing changes such as upgrades by using my latest Bash shell script! I took my <a href="https://blog.jennasrunbooks.com/azure-kubernetes-cluster-checkouts-bash-script">AKS checkouts script</a> from a couple of weeks ago and converted it into an EKS checkouts script 😎</p>
<p><strong>This script performs the following tasks:</strong></p>
<p>✅ Sets the kubeconfig context based on the provided cluster name (names must match)</p>
<p>✅ Display addresses of the master and services</p>
<p>✅ Checks the k8's version running on both the control plane and the nodes</p>
<p>✅ Checks supported EKS versions</p>
<p>✅ Lists all nodes and their status, age and version</p>
<p>✅ Display custom columns for pods with node, pod, namespace, status, and age</p>
<p>✅ Output count for total # pods running</p>
<p>✅ Gets all pods in the cluster, loops through each and runs a verbose describe outputting values from Node to Status</p>
<p><a href="https://github.com/jksprattler/kubernetes/blob/main/eks/scripts/eks-checkouts.sh"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686069598322/aa4bee0a-335e-40b2-b7aa-9d7f7e3fb76d.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p><em>Link to the script:</em> <a href="https://github.com/jksprattler/kubernetes/blob/main/eks/scripts/eks-checkouts.sh">https://github.com/jksprattler/kubernetes/blob/main/eks/scripts/eks-checkouts.sh</a></p>
<h2><strong>Usage &amp; Sample Output</strong></h2>
<p>The script output can be useful during instances where you need to submit a vendor ticket for troubleshooting. I'll also be including the output in my internal ticket tracking upgrades to validate it was completed.</p>
<p>Here's some sanitized sample output with redactions for brevity from running the script:</p>
<pre><code class="language-bash">❯ ./eks-checkouts.sh --help
Usage: ./eks-checkouts.sh &lt;cluster-name&gt; &lt;region&gt; &lt;aws-profile&gt;

❯ ./eks-checkouts.sh my-cluster my-region my-profile
Switched to context "my-cluster".
########################################################
Kubernetes control plane is running at https://my-cluster.gr7.my-region.eks.amazonaws.com
CoreDNS is running at https://my-cluster.gr7.my-region.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
########################################################
Control Plane Version: 
Client Version: v1.23.6
Server Version: v1.23.17-eks-0a21954
########################################################
Cluster Version: v1.23.16-eks-48e63af
Cluster Status: ACTIVE
########################################################
Available EKS versions:
1.22
1.23
1.24
1.25
1.26
1.27
########################################################
NAME                                         STATUS   ROLES    AGE   VERSION
ip-my-node.my-region.compute.internal   Ready    &lt;none&gt;   73d   v1.23.16-eks-48e63af
ip-my-node.my-region.compute.internal   Ready    &lt;none&gt;   73d   v1.23.16-eks-48e63af
#### Redacted for brevity ####
########################################################
NODE                                         POD                                             NAMESPACE      STATUS    AGE
ip-my-node.my-region.compute.internal   aws-node-zs123                                  kube-system    Running   2023-03-24T18:35:15Z
#### Redacted for brevity ####
########################################################
Total number of pods running:      163
########################################################
Checking pod: cert-manager/cert-manager-12345678-45svg
Node:         ip-my-node.my-region.compute.internal/ip-my-node
Start Time:   Thu, 16 Mar 2023 06:25:19 -0500
Labels:       app=cert-manager
              app.kubernetes.io/component=controller
              app.kubernetes.io/instance=cert-manager
              app.kubernetes.io/name=cert-manager
              app.kubernetes.io/version=v1.8.2
              pod-template-hash=12345678
Annotations:  kubernetes.io/psp: eks.privileged
              prometheus.io/path: /metrics
              prometheus.io/port: 9090
              prometheus.io/scrape: true
Status:       Running
########################################################
#### Redacted for brevity ####
</code></pre>
<p>Since I'm performing upgrades to my EKS clusters this week, I decided to put this script together and share it! Enjoy!</p>
]]></content:encoded></item><item><title><![CDATA[AWS Find AMIs | Boto3 Script]]></title><description><![CDATA[Overview
Capture a list of available AWS AMIs for a specified region and OS version with my latest boto3 script. This Python script will output a color-coded table listing AMIs that have been released]]></description><link>https://blog.jennasrunbooks.com/aws-find-amis-boto3-script</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/aws-find-amis-boto3-script</guid><category><![CDATA[AWS]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[Python]]></category><category><![CDATA[boto3]]></category><category><![CDATA[Cloud]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Wed, 31 May 2023 13:57:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/M5tzZtFCOfs/upload/f18071d9a0552c1e31c02c3fc5d508ce.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Overview</h2>
<p>Capture a list of available AWS AMIs for a specified region and OS version with my latest boto3 script. This Python script will output a color-coded table listing AMIs that have been released within the last 30 days with the owner alias set to Amazon. The table includes columns for AMI ID, Name, Architecture type and Virtualization type. Output is sorted by Architecture type (x86_64, arm64, etc), then sorted by AMI names which include release dates.</p>
<p>Sample output against <code>us-west-2</code> region for a list of <code>debian-11*</code> AMI's:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1685493555879/b45c934e-5ef8-431a-abed-2f9a86319cec.png" alt="" style="display:block;margin:0 auto" />

<h2>Purpose</h2>
<p>For my particular use case, I have a dev environment where AMIs for various OS versions and instance types need to be built for testing purposes. I also have a fleet of production instances that all run the same OS version and machine type using a custom AWS Terraform module. I'm still able to use this same Terraform module for my dev infrastructure however, I've created a unique variable to be able to assign a specific AMI to certain dev resources.  The <code>user_specified_ami</code> var is what I'm using to assign my AMIs.  I've applied the coalesce function to the "ami" resource which forces this value to return as precedent over the next sequential <a href="http://data.aws">data.aws</a>_ami source as long as there's a string assigned to this variable. It looks like this in the aws_instance resource block of the module:</p>
<p><code>ami = coalesce(var.user_specified_ami,</code> <a href="http://data.aws"><code>data.aws</code></a><code>_</code><a href="http://ami.prod"><code>ami.prod</code></a><code>_</code><a href="http://os.id"><code>os.id</code></a><code>)</code></p>
<p>Then in my configuration module, I can simply assign the user_specified_ami an ID from the script output.</p>
<h2>Script Functionality</h2>
<p><a href="https://github.com/jksprattler/aws-tools/blob/main/scripts/aws_ec2_find_amis.py"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1685539900686/5b5466cd-6987-4c00-a6bc-4cabbf7bfd7d.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p><em>Link to the script:</em> <a href="https://github.com/jksprattler/aws-tools/blob/main/scripts/aws_ec2_find_amis.py">https://github.com/jksprattler/aws-tools/blob/main/scripts/aws_ec2_find_amis.py</a></p>
<h3>Usage</h3>
<p>Here's a look at the options available from the <code>-h/--help</code> output:</p>
<pre><code class="language-bash">❯ python aws_ec2_find_amis.py --help
usage: aws_ec2_find_amis.py [-h] region os_version

Find AMIs based on region and OS

positional arguments:
  region      AWS region
  os_version  OS version

options:
  -h, --help  show this help message and exit
</code></pre>
<p>Here's an example of entering an OS version not set in the <code>valid_os_versions</code> list of elements:</p>
<pre><code class="language-bash">❯ python aws_ec2_find_amis.py us-west-2 debian
Invalid OS version 'debian'. Available OS versions:
amzn2
debian-11
ubuntu-jammy
Windows_Server-2019-English
</code></pre>
<p>Here's an example of entering an invalid AWS region:</p>
<pre><code class="language-bash">❯ python aws_ec2_find_amis.py us-west debian-11
Invalid region 'us-west'. Available regions:
af-south-1
ap-south-1
eu-north-1
eu-west-3
eu-south-1
eu-west-2
eu-west-1
ap-northeast-3
ap-northeast-2
me-south-1
ap-northeast-1
ca-central-1
sa-east-1
ap-east-1
ap-southeast-1
ap-southeast-2
eu-central-1
ap-southeast-3
us-east-1
us-east-2
us-west-1
us-west-2
</code></pre>
<h3>Code Explanation</h3>
<p>Inside the main function, it calls the <code>parse_arguments()</code> function to parse the user input arguments and assigns them to the <code>region</code> and <code>os_version</code> variables. See the above help output for example.</p>
<pre><code class="language-python">def main():
    """
    Parse user arguments, assign region and os values
    Lookup latest available AMI's and output them to color-coded table
    """
    region, os_version = parse_arguments()
    validate_inputs(region, os_version)
    find_amis(region, os_version)
</code></pre>
<p>Here's the parse_arguments() function :</p>
<pre><code class="language-python">def parse_arguments():
    """ Parse args """
    parser = argparse.ArgumentParser(description='Find AMIs based on region and OS')
    parser.add_argument('region', type=str, help='AWS region')
    parser.add_argument('os_version', type=str, help='OS version')
    args = parser.parse_args()
    return args.region, args.os_version
</code></pre>
<p>Next, it checks if the provided inputs for the region and os_version arguments are valid with the <code>validate_inputs(region, os_version)</code> function.</p>
<pre><code class="language-python">def validate_inputs(region, os_version):
    """ Validate region and OS version if provided"""
    if region:
        available_regions = get_available_regions()
        if region not in available_regions:
            print(f"Invalid region '{region}'. Available regions:")
            for regions in available_regions:
                print(regions)
            sys.exit()

    if os_version:
        validate_os_version(os_version)
</code></pre>
<p>This will perform a conditional check for the region input against the <code>get_available_regions()</code> function which performs an API call using the EC2 client to describe the AWS regions:</p>
<pre><code class="language-python">def get_available_regions():
    """
    Get a list of available AWS regions
    """
    ec2_client = boto3.client('ec2')
    response = ec2_client.describe_regions()
    regions = [region['RegionName'] for region in response['Regions']]
    return regions
</code></pre>
<p>Then it performs a conditional check for the os_version input against the <code>validate_os_version(os_version)</code> function which checks if the provided versions starts with any of the elements in the <code>valid_os_versions</code> list:</p>
<pre><code class="language-python">def validate_os_version(os_version):
    """ Check if the provided OS version is valid """
    valid_os_versions = ['amzn2', 'debian-11', 'ubuntu-jammy', 'Windows_Server-2019-English']
    for version in valid_os_versions:
        if os_version.startswith(version):
            return
    print(f"Invalid OS version '{os_version}'. Available OS versions:")
    for version in valid_os_versions:
        print(version)
    sys.exit()
</code></pre>
<p>Finally, the <code>find_amis(region, os_version)</code> function is executed. The API of the EC2 client is called to describe images hosted in the specified region and filtered based on the provided OS version and AMI alias owner, Amazon:</p>
<pre><code class="language-python">def find_amis(region, os_version):
    """
    Output a list of the latest Amazon owned AMI's for the specified
    region and OS version in color-coded Table format
    """

    ec2_client = boto3.client('ec2', region_name=region)

    response = ec2_client.describe_images(
        Filters=[
            {'Name': 'name', 'Values': ['*'+os_version+'*']},
            {'Name': 'owner-alias', 'Values': ['amazon']}
        ]
    )

    amis = response['Images']

    # Sort the AMIs by architecture type and name
    sorted_amis = sorted(amis, key=lambda x: (x['Architecture'], x['Name']))
    latest_amis = []
    for ami in sorted_amis:
        creation_date = datetime.datetime.strptime(ami['CreationDate'], "%Y-%m-%dT%H:%M:%S.%fZ")
        if creation_date &gt;= DATE_THRESHOLD:
            latest_amis.append(ami)

    table = []
    table_headers = ["AMI ID", "Name", "Architecture", "VirtualizationType"]
    for ami in latest_amis:
        table.append([
            f"{Fore.CYAN}{ami['ImageId']}{Style.RESET_ALL}",
            ami['Name'],
            f"{Fore.GREEN}{ami['Architecture']}{Style.RESET_ALL}",
            ami['VirtualizationType']
        ])
    print(tabulate(table, headers=table_headers, tablefmt="grid"))
</code></pre>
<p>The response contains a list of AMIS matching these filters and is sorted by the Architecture type. Further sorting is done by AMI name which contains release dates. Only AMIs with release dates in the last 30 days are provided.</p>
<p>Next, a table is generated by creating an empty list called <code>table</code> to store the AMI information for tabulation. As the script iterates over the sorted AMIs, the requested data for the AMI ID, Name, Architecture and Virtualization Type are appended to the <code>table</code> list. The ImageID and the Architecture values have color formatting applied using the Fore class from the colorama library. I added this feature to help with the readability of the table output.</p>
<h2>Conclusion</h2>
<p>Now I'm able to get these AMI values much more quickly with this handy script rather than crafting an <code>aws ec2 describe-images</code> command and parsing through the output. Hope you found this useful! What kind of use cases do you have for capturing and assigning AMIs?</p>
]]></content:encoded></item><item><title><![CDATA[AWS Lambda Function: IAM User Password Expiry Notice | SES, Boto3 & Terraform]]></title><description><![CDATA[Overview
In this implementation, you'll be guided through the necessary steps to set up an AWS Lambda function to email notifications to IAM Users when their AWS Web Console passwords are expiring. Th]]></description><link>https://blog.jennasrunbooks.com/aws-lambda-function-iam-user-password-expiry-notice-ses-boto3-terraform</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/aws-lambda-function-iam-user-password-expiry-notice-ses-boto3-terraform</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws lambda]]></category><category><![CDATA[Python]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Mon, 22 May 2023 20:59:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/eVaxJVA2zHI/upload/03fa98ea5a2453839fd114e74b80bb04.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Overview</h2>
<p>In this implementation, you'll be guided through the necessary steps to set up an AWS Lambda function to email notifications to IAM Users when their AWS Web Console passwords are expiring. The function is written in Python (boto3) and integrated with AWS SES using a verified domain. Terraform code samples are provided for all of the infrastructure configuration steps.</p>
<p>The purpose of this initiative is to provide proactive security measures while streamlining administrative tasks by sending an advanced notice to AWS IAM users that their passwords are expiring. By promptly alerting users before their passwords reach the expiration threshold defined in the security policy, you can facilitate regular password resets, ensuring stronger security measures are maintained. This approach reduces the administrative burden on administrators, as users can self-reset their passwords in advance of expiration. Additionally, it empowers users to take ownership of their account security, promoting a culture of proactive password management.</p>
<p>With this implementation, you will enhance the overall security, efficiency, and user experience within your AWS IAM environment.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684786661947/ba4260ee-d726-4dd0-b023-2777d30d9bf0.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>Pre-requisites</strong></h2>
<ul>
<li><p>Domain ownership and access to manage DNS records</p>
<ul>
<li>Domain <code>jennasrunbooks.com</code> will be used as an example in the below sample code output. Update this with your appropriate domain.</li>
</ul>
</li>
<li><p>Terraform installed</p>
</li>
<li><p>Install Python, boto3 and other script modules as needed to review the code locally without errors</p>
</li>
<li><p>Clone <a href="https://github.com/jksprattler/aws-security">repo</a> containing Lambda Python script</p>
</li>
<li><p>AWS IAM permissions to deploy Terraform resources</p>
</li>
<li><p>Email tags assigned to each AWS IAM User, sample Terraform code from a terraform.tfvars file:</p>
<pre><code class="language-bash">user_names = {
  "user1" = {
    "name" = "user1",
    "tag"  = { email = "user1@jennasrunbooks.com", role = "engineering" }
  }
}
</code></pre>
</li>
</ul>
<h2>Procedure</h2>
<h3>Set up and verify the email domain</h3>
<ul>
<li><p>Register and <a href="https://docs.aws.amazon.com/ses/latest/dg/verify-addresses-and-domains.html">verify your email domain</a> in the Amazon SES console to establish your email sender identity.</p>
</li>
<li><p>If you'd like to use a custom MAIL FROM domain, check out this <a href="https://docs.aws.amazon.com/ses/latest/dg/mail-from.html">AWS doc</a> and note the following:</p>
<details>
<summary>Important</summary>
<p>To successfully set up a custom MAIL FROM domain with Amazon SES, you must publish exactly one MX record to the DNS server of your MAIL FROM domain. If the MAIL FROM domain has multiple MX records, the custom MAIL FROM setup with Amazon SES will fail.</p>
</details></li>
<li><p>Sample Terraform code to deploy an AWS SES domain identity using Easy DKIM settings and a custom MAIL FROM domain:</p>
<pre><code class="language-bash"># Update the domain variable to your domain
# using the SES service which is integrated into other services ie Lambda

variable "domain" {
  type    = string
  default = "jennasrunbooks.com"
}

resource "aws_ses_domain_identity" "ses_domain" {
  domain = var.domain
}

resource "aws_ses_domain_mail_from" "main" {
  domain           = aws_ses_domain_identity.ses_domain.domain
  mail_from_domain = "mail.${var.domain}"
}

resource "aws_ses_domain_identity_verification" "email_identity_verification" {
  domain = aws_ses_domain_identity.ses_domain.domain
}
</code></pre>
<ul>
<li><p>The initial terraform apply will likely timeout on the <code>aws_ses_domain_identity_verification</code> resource depending on how quickly your DNS provider can propagate the changes to verify the domain identity with AWS. You can simply rerun an apply to update the state once the domain has been verified.</p>
</li>
<li><p>Update your DNS provider records for your domain with the DKIM CNAME and MAIL FROM MX and SPF records provided by AWS as shown in this example after running <code>terraform apply</code>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684680690711/a90c9ae2-8c6c-457e-a5e0-96dd328a8999.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>You have the option to download a csv of each record set which can be useful if another team or 3rd party manages the records for your domain.</p>
</li>
<li><p>DNS propagation can take anywhere from a few minutes to several hours. If entered correctly, the SES page for the domain verification will display the identity status as <em>Verified</em>. Once verified, you'll also receive an email from AWS for each configuration type you've configured.</p>
</li>
</ul>
</li>
</ul>
<h3>Write the Lambda function Python script</h3>
<p><a href="https://github.com/jksprattler/aws-security/blob/main/lambda/password_notification/password_notification.py"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684711881093/ba32523c-0a62-47fd-8f36-fe4df03437de.png" alt="" style="display:block;margin:0 auto" /></a></p>
<ul>
<li><p>Link to the script: <a href="https://github.com/jksprattler/aws-security/blob/main/lambda/password_notification/password_notification.py">https://github.com/jksprattler/aws-security/blob/main/lambda/password_notification/password_notification.py</a></p>
</li>
<li><p>Only IAM users with valid <code>email</code> tags using the verified domain in their address will be notified.</p>
</li>
<li><p>Update the SES email body contents with an appropriate message for your audience.</p>
</li>
<li><p>Include a direct link to the account's AWS console sign-in page. If you have multiple accounts used in your infrastructure, it can also be helpful to specify to your clients which account the password reset is for by providing both the friendly name and account# in the message body.</p>
</li>
<li><p>Update the Source email account in the <code>ses_client.send_email</code> method as appropriate to your cloud admins team Distribution List.</p>
</li>
</ul>
<h3>Configure the Lambda IAM policy and role</h3>
<ul>
<li><p>Create an IAM policy that includes the necessary permissions for the Lambda function to generate and get the credential report, send emails using SES, and access user tags (we specifically need the email tags for each user).</p>
</li>
<li><p>Give the policy permissions to create the log group, and log streams and push log events to the streams. This will be used by CloudWatch when it's triggered on the scheduled event.</p>
</li>
<li><p>Create an IAM role and attach the IAM policy to it. This role will be assumed by the Lambda function to access the required AWS services.</p>
</li>
<li><p>Sample Terraform code to create the IAM policy, role and policy attachment needed for the lambda function:</p>
<pre><code class="language-bash"># Update Resource ARN references to region, accountID and aws lambda log-group name as needed for your environment

resource "aws_iam_policy" "lambda_password_notification_policy" {
  name   = "lambda_password_notification_policy"
  policy = &lt;&lt;EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "VisualEditor0",
          "Effect": "Allow",
          "Action": [
            "iam:GenerateCredentialReport",
            "ses:SendEmail",
            "ses:SendRawEmail",
            "iam:GetCredentialReport",
            "iam:ListUserTags"
          ],
          "Resource": "*"
        },
        {
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogGroup"
          ],
          "Resource": "arn:aws:logs:region:accountID:*"
        }
      ]
    },
        {
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ],
          "Resource": "arn:aws:logs:region:accountID:log-group:/aws/lambda/password_notification:*"
        }
      ]
    }
    EOF
  tags   = var.common_tags
}

resource "aws_iam_role" "lambda_password_notification_role" {
  name               = "lambda_password_notification_role"
  assume_role_policy = &lt;&lt;EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "",
          "Effect": "Allow",
          "Principal": {
            "Service": "lambda.amazonaws.com"
          },
          "Action": "sts:AssumeRole"
        }
      ]
    }
    EOF
}

resource "aws_iam_role_policy_attachment" "lambda_policy_attachment" {
  role       = aws_iam_role.lambda_password_notification_role.name
  policy_arn = aws_iam_policy.lambda_password_notification_policy.arn
}
</code></pre>
</li>
</ul>
<h3>Setup the Lambda function</h3>
<ul>
<li><p>Create the new Lambda function with Python (boto3) to generate SES email notifications of expired passwords by generating and parsing the IAM credentials report using Terraform to deploy the resources.</p>
</li>
<li><p>Python 3.8 will be used for the runtime</p>
</li>
<li><p>The handler will use the name of the function, ie: <code>password_notification.lambda_handler</code></p>
</li>
<li><p>The previously created IAM role will be assigned to the Lambda function</p>
</li>
<li><p>The Python script will be zipped using the Terraform <code>archive_file</code> data source</p>
</li>
<li><p>The <code>source_code_hash</code> argument will be applied to the <code>aws_lambda_function</code> resource block to trigger updates in the terraform state when changes are made to the Python script.</p>
</li>
<li><p>Sample Terraform code:</p>
<pre><code class="language-bash"># Update below file paths according to your directory architecture

data "aws_lambda_function" "password_notification" {
  function_name = aws_lambda_function.password_notification.function_name
}

data "archive_file" "password_notification_zip" {
  type        = "zip"
  source_dir  = "src_dir/scripts/aws/lambda/password_notification/"
  output_path = "dest_dir/scripts/aws/lambda/password_notification/password_notification.zip"
}

resource "aws_lambda_function" "password_notification" {
  filename         = "dest_dir/scripts/aws/lambda/password_notification/password_notification.zip"
  source_code_hash = data.archive_file.password_notification_zip.output_base64sha256
  function_name    = "password_notification"
  description      = "lambda function to send email notifications (SES) to users when passwords are expiring"
  role             = aws_iam_role.lambda_password_notification_role.arn
  handler          = "password_notification.lambda_handler"
  runtime          = "python3.8"
  timeout          = 180
  memory_size      = 128
  depends_on = [
    aws_iam_role_policy_attachment.lambda_policy_attachment,
    aws_cloudwatch_log_group.password_notification_log_group,
  ]
}
</code></pre>
</li>
</ul>
<h3>Test the Lambda function</h3>
<ul>
<li><p><a href="https://docs.aws.amazon.com/lambda/latest/dg/testing-functions.html">Test</a> the Lambda function manually by triggering it with sample input data to ensure it sends the correct email notifications.</p>
</li>
<li><p>Use the existing lambda function deployed previously or create a new one using the same settings, IAM role, etc for this test.</p>
</li>
<li><p>Use the Lambda console to manually test the function.</p>
</li>
<li><p>Example updated Lambda Python to force a test email notification using custom JSON event values:</p>
<pre><code class="language-python">"""
Jenna Sprattler | SRE Kentik | 2023-05-21
Test lambda function to send email notifications (SES) to specified test user that passwords are expiring
"""
import time
from datetime import datetime
from dateutil import parser
import boto3

iam_client = boto3.client('iam')
ses_client = boto3.client('ses')

def lambda_handler(event, context):
    username = event['username']
    password_last_changed = event['password_last_changed']
    email = event['email']

    if password_last_changed not in ('N/A', 'not_supported'):
        # Parse the date and time components from the timestamp
        password_last_changed_date = parser.parse(password_last_changed)
        days_since_password_change = (
            datetime.now() - password_last_changed_date.replace(tzinfo=None)).days
        if days_since_password_change &gt; 78:
            message = f'''
                &lt;html&gt;
                &lt;body&gt;
                &lt;p&gt;Hello {username},&lt;/p&gt;
                &lt;p&gt;Your password to access the &lt;a href="https://signin.aws.amazon.com/console"&gt;AWS web console&lt;/a&gt; has expired or will be expiring within the next 12 days.&lt;/p&gt;
                &lt;p&gt;If your password is still valid, please log into the web console and follow the banner instructions to reset your password now.&lt;/p&gt;
                &lt;p&gt;If you have API access keys configured, you can use the Password Reset Self Service &lt;a href="https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_self_service_password_reset.py"&gt;script&lt;/a&gt;.&lt;/p&gt;
                &lt;p&gt;If you don't use API keys, and your password has passed the expiration date then reply to this email and we'll assist with your password reset.&lt;/p&gt;
                &lt;p&gt;If you don't use your AWS account, reply to this email and we'll remove your account.&lt;/p&gt;
                &lt;br&gt;
                    &lt;p&gt;Thank you,&lt;/p&gt;
                    &lt;p&gt;Cloud Admins&lt;/p&gt;
                &lt;/body&gt;
                &lt;/html&gt;
                '''
            ses_client.send_email(
                Source='cloud-admins@jennasrunbooks.com',
                Destination={'ToAddresses': [email]},
                Message={
                    'Subject': {'Data': 'AWS Password Expiry Notification'},
                    'Body': {'Html': {'Data': message}}
                }
            )
    return 'Password expiry notifications sent to: ' + username
</code></pre>
<ul>
<li><p>Add appropriate JSON test event values to your variables and generate a test SES email notification, for example:</p>
<pre><code class="language-json">{
  "username": "user1",
  "password_last_changed": "2023-02-10T00:00:00Z",
  "email": "user1@jennasrunbooks.com"
}
</code></pre>
</li>
<li><p>Validate that the test email was received by the test user's email account. Review the message subject and body contents for accuracy. Sample test email to my AWS IAM user1:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684773403829/6bc5f023-8e8b-4ae7-bb78-edbc9ce0478c.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Feel free to use the Password Reset Self-Service <a href="https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_self_service_password_reset.py">script</a> referenced in the above email body.</p>
</li>
<li><p>For users with passwords passed the expiration date, check out my Administrative user password reset <a href="https://blog.jennasrunbooks.com/boto3-script-to-reset-aws-iam-user-passwords">script</a>.</p>
</li>
</ul>
</li>
</ul>
<h3>Setup the CloudWatch event rule</h3>
<ul>
<li><p>Create the CloudWatch Event Rule to trigger the desired schedule for automating the password expiration notification Lambda function which contains the SES <code>send_email</code> operation to your users with expiring passwords.</p>
</li>
<li><p>Set the target of the CloudWatch Event Rule to the Lambda function you created.</p>
</li>
<li><p>(Optional) Create the log group for the CloudWatch event log streams. This is created automatically whenever a new Lambda function is deployed however, if/when you destroy your Lambda function in the future, Terraform will not automatically delete the log group that was created. If the log group resource is managed by the Terraform state then you will know to delete that resource and have cleaner infrastructure should the Lambda function be removed in the future.</p>
</li>
<li><p>There is a hidden Terraform resource called <code>aws_lambda_permission</code> that needs to be created. This is created automatically when a Lambda function is created using the AWS console. This allows the CloudWatch events permissions to access the Lambda function. This is needed for the CloudWatch event rule (cron scheduler), log streams, etc to work properly with the function.</p>
</li>
<li><p>For testing purposes, you could update the <code>schedule_expression</code> of the event rule to run every 10 min while confirming that the Lambda function and Cloudwatch settings are working properly then update to a daily interval: <code>rate(10 minutes)</code></p>
</li>
<li><p>Sample Terraform code:</p>
<pre><code class="language-bash">resource "aws_lambda_permission" "allow_cloudwatch_for_password_notification" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.password_notification.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.password_notification_schedule.arn
}

resource "aws_cloudwatch_log_group" "password_notification_log_group" {
  name              = "/aws/lambda/password_notification"
  retention_in_days = 7
}

resource "aws_cloudwatch_event_rule" "password_notification_schedule" {
  name                = "password_notification_schedule"
  description         = "Scheduled rule for password notification"
  schedule_expression = "cron(0 13 * * ? *)" # Schedule for 8am CST / 1pm UTC daily (adjust as needed)
}

resource "aws_cloudwatch_event_target" "password_notification_target" {
  rule = aws_cloudwatch_event_rule.password_notification_schedule.name
  arn  = data.aws_lambda_function.password_notification.arn
}
</code></pre>
</li>
</ul>
<h3>Validate functionality</h3>
<ul>
<li><p>Monitor the CloudWatch logs in the designated Lambda function's log group and check for any errors or unexpected behavior. Example of a successful run:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684777829941/2b00f99f-18c8-4aa3-9061-c2a90f3a6b5a.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Verify that the Lambda function is generating the credential report and sending the email notifications to the intended recipients.</p>
</li>
<li><p>You should see an IAM credentials report created shortly after the Lambda function's scheduled event, for example:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684777319944/e67988e2-06ca-40f5-a55a-e0c2563e705a.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>You can download this report and sort by the <code>password_last_changed</code> column to capture a list of users that should have received the SES notification. Reach out to these users to confirm whether or not they received the email notification.</p>
</li>
</ul>
<h2>Conclusion</h2>
<p>This implementation provides a comprehensive solution for notifying AWS IAM users of their upcoming password expirations for accessing the AWS web console. With this proactive approach to security, password hygiene is prioritized while also helping reduce the risk of compromised credentials.</p>
<p>Empowering users to self-reset their passwords before expiration not only enhances account security but also streamlines administrative tasks. This implementation showcases the value of leveraging automation and user-centric design to create a more secure and efficient AWS IAM environment. With this solution, organizations can foster a culture of proactive password management.</p>
<h2>Resources</h2>
<ul>
<li><p><strong>Domain Identities</strong></p>
<ul>
<li><p>Verified identities in Amazon SES - Amazon Simple Email Service: <a href="https://docs.aws.amazon.com/ses/latest/dg/verify-addresses-and-domains.html">https://docs.aws.amazon.com/ses/latest/dg/verify-addresses-and-domains.html</a></p>
</li>
<li><p>Using a custom MAIL FROM domain - Amazon Simple Email Service: <a href="https://docs.aws.amazon.com/ses/latest/dg/mail-from.html">https://docs.aws.amazon.com/ses/latest/dg/mail-from.html</a></p>
</li>
</ul>
</li>
<li><p><strong>Lambda Function</strong></p>
<ul>
<li><p>aws_lambda_function | Resources | hashicorp/aws | Terraform Registry: <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function#iam-role">https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function#iam-role</a></p>
</li>
<li><p>Lambda execution role - AWS Lambda: <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html">https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html</a></p>
</li>
<li><p>Getting started with Lambda - AWS Lambda: <a href="https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html">https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html</a></p>
</li>
<li><p>Building Lambda functions with Python - AWS Lambda: <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html">https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html</a></p>
</li>
<li><p>Testing Lambda functions in the console - AWS Lambda: <a href="https://docs.aws.amazon.com/lambda/latest/dg/testing-functions.html">https://docs.aws.amazon.com/lambda/latest/dg/testing-functions.html</a></p>
</li>
<li><p>Monitoring and troubleshooting Lambda functions - AWS Lambda: <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-monitoring.html">https://docs.aws.amazon.com/lambda/latest/dg/lambda-monitoring.html</a></p>
</li>
</ul>
</li>
<li><p><strong>CloudWatch Events</strong></p>
<ul>
<li><p>aws_cloudwatch_event_rule | Resources | hashicorp/aws | Terraform Registry: <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule">https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule</a></p>
</li>
<li><p>Schedule Expressions for Rules - Amazon CloudWatch Events: <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html">https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html</a></p>
</li>
</ul>
</li>
<li><p><strong>Boto3</strong></p>
<ul>
<li><p>generate_credential_report - Boto3 1.26.137 documentation: <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/generate_credential_report.html">https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/generate_credential_report.html</a></p>
</li>
<li><p>get_credential_report - Boto3 1.26.137 documentation: <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/get_credential_report.html">https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/get_credential_report.html</a></p>
</li>
<li><p>list_user_tags - Boto3 1.26.137 documentation: <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/list_user_tags.html">https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/list_user_tags.html</a></p>
</li>
<li><p>send_email - Boto3 1.26.137 documentation: <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses/client/send_email.html">https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses/client/send_email.html</a></p>
</li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Azure Kubernetes Cluster Checkouts | Bash Script]]></title><description><![CDATA[Overview
Perform checkouts against your Azure Kubernetes cluster before and after implementing changes such as upgrades by using this Bash shell script I wrote.
This script provides output for the fol]]></description><link>https://blog.jennasrunbooks.com/azure-kubernetes-cluster-checkouts-bash-script</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/azure-kubernetes-cluster-checkouts-bash-script</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[aks]]></category><category><![CDATA[kubectl]]></category><category><![CDATA[Script]]></category><category><![CDATA[Azure]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Wed, 17 May 2023 18:33:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Esq0ovRY-Zs/upload/2236ff038c272b0b675e1c5f94245204.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Overview</h2>
<p>Perform checkouts against your Azure Kubernetes cluster before and after implementing changes such as upgrades by using this Bash shell script I wrote.</p>
<p><strong>This script provides output for the following checks:</strong></p>
<p>✅ Sets the kubeconfig context based on the provided cluster name (names must match)</p>
<p>✅ Display addresses of the master and services</p>
<p>✅ Checks running version of control master and available upgrade path</p>
<p>✅ Checks the kubernetes version running on both the control plane and the node pools</p>
<p>✅ Lists all node pools in the cluster</p>
<p>✅ Lists all nodes running across both pools in the cluster</p>
<p>✅ Output count for total # pods running</p>
<p>✅ Gets all pods in the cluster, loops through each and runs a verbose describe outputting values from Node to Status</p>
<p><a href="https://github.com/jksprattler/kubernetes/blob/main/aks/scripts/aks-checkouts.sh"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684348247241/c0a84fb2-af31-447a-9d41-98fc5b59c04a.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>Link to the script: <a href="https://github.com/jksprattler/kubernetes/blob/main/aks/scripts/aks-checkouts.sh">https://github.com/jksprattler/kubernetes/blob/main/aks/scripts/aks-checkouts.sh</a></p>
<h2>Usage &amp; Sample Output</h2>
<p>The script output can be useful during instances where you need to submit a vendor ticket for troubleshooting. I'll also be including the output in my internal ticket tracking the upgrade to validate it was completed.</p>
<p>Here's some sanitized sample output with redactions for brevity from running the script:</p>
<pre><code class="language-bash">❯ ./aks-checkouts.sh my-rg my-cluster
Switched to context "my-cluster".
########################################################
Kubernetes control plane is running at https://my-cluster-00000000.hcp.westus2.azmk8s.io:443
CoreDNS is running at https://my-cluster-00000000.hcp.westus2.azmk8s.io:443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://my-cluster-00000000.hcp.westus2.azmk8s.io:443/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
########################################################
Name     ResourceGroup    MasterVersion    Upgrades
-------  ---------------  ---------------  -----------------------
default  my-rg        1.24.9           1.24.10, 1.25.5, 1.25.6
########################################################
Name                  Location    ResourceGroup    KubernetesVersion    CurrentKubernetesVersion    ProvisioningState    Fqdn
--------------------  ----------  ---------------  -------------------  --------------------------  -------------------  ---------------------------------------------------
my-cluster  westus2     my-rg        1.24.9               1.24.9                      Succeeded            my-cluster-00000000.hcp.westus2.azmk8s.io
########################################################
Name    OsType    KubernetesVersion    VmSize            Count    MaxPods    ProvisioningState    Mode
------  --------  -------------------  ----------------  -------  ---------  -------------------  ------
user  Linux     1.24.9               Standard_DS13_v2  11       30         Succeeded            User
system  Linux     1.24.9               Standard_DS2_v2   3        30         Succeeded            System
########################################################
NAME                             STATUS   ROLES   AGE   VERSION
aks-user-99999999-vmss000000   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss000005   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss000006   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss000007   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss00000a   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss00000d   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss00000k   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss00000o   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss00000p   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss00000q   Ready    agent   20h   v1.24.9
aks-user-99999999-vmss00000s   Ready    agent   20h   v1.24.9
aks-system-99999999-vmss000000   Ready    agent   21h   v1.24.9
aks-system-99999999-vmss000001   Ready    agent   21h   v1.24.9
aks-system-99999999-vmss000002   Ready    agent   21h   v1.24.9
########################################################
NODE                             POD                                                      NAMESPACE     STATUS    AGE
aks-user-99999999-vmss000000   ama-logs-abcde                                           kube-system   Running   2023-05-18T15:19:14Z
aks-user-99999999-vmss000005   ama-logs-fghij                                           kube-system   Running   2023-05-18T15:46:55Z
aks-system-99999999-vmss000000   ama-logs-klmno                                           kube-system   Running   2023-05-18T14:42:46Z
aks-system-99999999-vmss000001   ama-logs-pqrst                                           kube-system   Running   2023-05-18T14:46:15Z
#### Redacted for brevity ####
########################################################
Total number of pods running:      217
########################################################
Checking pod: botkube/botkube-99999999-abcde
Node:         aks-user-99999999-vmss000000/1.1.1.1
Start Time:   Thu, 18 May 2023 10:24:44 -0500
Labels:       app=botkube
              component=controller
              pod-template-hash=99999999
Annotations:  &lt;none&gt;
Status:       Running
########################################################
Checking pod: default/filebeat-fghij
Node:         aks-user-99999999-vmss000007/1.1.1.2
Start Time:   Thu, 18 May 2023 10:34:15 -0500
Labels:       app=filebeat
              controller-revision-hash=99999999
              pod-template-generation=3
Annotations:  kubectl.kubernetes.io/restartedAt: 2023-04-13T09:36:26-10:00
Status:       Running
########################################################
#### Redacted for brevity ####
</code></pre>
<p>Hope you found this useful! What kinds of tools do you use in your Kubernetes environment for running checkouts against changes like upgrades?</p>
]]></content:encoded></item><item><title><![CDATA[AWS Access Key rotation for IAM Users | Boto3 Script]]></title><description><![CDATA[Overview
Rotate your AWS API access keys using this Boto3 script for IAM users that I wrote. Optional arguments include create, update, delete, and list access keys. When applying the create new 🔑 fe]]></description><link>https://blog.jennasrunbooks.com/aws-access-key-rotation-for-iam-users-boto3-script</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/aws-access-key-rotation-for-iam-users-boto3-script</guid><category><![CDATA[AWS]]></category><category><![CDATA[Python]]></category><category><![CDATA[boto3]]></category><category><![CDATA[awssecurity]]></category><category><![CDATA[cloudsecurity]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Thu, 13 Apr 2023 21:20:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/C1P4wHhQbjM/upload/432c39e9160f33f82385d9f6221fa042.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Overview</h2>
<p>Rotate your AWS API access keys using this Boto3 script for IAM users that I wrote. Optional arguments include create, update, delete, and list access keys. When applying the create new 🔑 feature, the user is prompted whether or not to overwrite their existing <code>~/.aws/credentials</code> file contents with the <code>[default]</code> profile, newly created access key ID and secret. If they enter 'n', the new key is still created however, the user will need to update their credentials file manually with the new key ID and secret when ready to use it locally.</p>
<p>Although <a href="https://aws.amazon.com/blogs/security/how-to-rotate-access-keys-for-iam-users/">this AWS blog post</a> is 10 years old, I still find it to be the most succinct procedure explaining how to rotate your access keys. Highlighting the general steps to follow from the post below which should be considered when using this script:</p>
<p>To rotate access keys, you should follow these steps:</p>
<ol>
<li><p>Create a second access key in addition to the one in use.</p>
</li>
<li><p>Update all your applications to use the new access key and validate applications are working.</p>
</li>
<li><p>Change the state of the previous access key to inactive.</p>
</li>
<li><p>Validate that your applications are still working as expected.</p>
</li>
<li><p>Delete the inactive access key.</p>
</li>
</ol>
<h2>Purpose</h2>
<p>AWS access keys should be treated like any other security credentials you use daily. Just as it's best practice to change your password regularly, the same treatment should be applied to your access keys. Regularly rotating your keys helps mitigate the potential risk of a security breach occurring within your cloud estate should your keys become compromised.</p>
<h2>Script Functionality</h2>
<p><a href="https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_self_service_key_rotation.py"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681343514290/8991879b-0b6e-4156-afd8-e46c297581e2.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p><em>Link to script:</em> <a href="https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_self_service_key_rotation.py">https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_self_service_key_rotation.py</a></p>
<p>Here's a look at the options available from the <code>-h/--help</code> output:</p>
<pre><code class="language-bash">❯ python aws_iam_self_service_key_rotation.py -h
usage: aws_iam_self_service_key_rotation.py [-h] [-c] [-u UPDATE UPDATE] [-d DELETE] [-l]

create|update|delete|list API access keys

optional arguments:
  -h, --help            show this help message and exit
  -c, --create          create access key
  -u UPDATE UPDATE, --update UPDATE UPDATE
                        update access key; 2 expected inputs: &lt;keyID&gt; &lt;active|inactive&gt;
  -d DELETE, --delete DELETE
                        delete access key; 1 expected input: &lt;keyID&gt;
  -l, --list            list access key ID, status, creation date
</code></pre>
<p>Here's a list of my current access key(s):</p>
<pre><code class="language-bash">❯ python aws_iam_self_service_key_rotation.py -l
Key:        ****************4NOP
Status:     Active
Created:    2023-02-19 14:51:31+00:00
</code></pre>
<p>Following the general steps outlined in the Overview section above, I'll go ahead and create a new access key since I only have 1 currently and there's a limit of 2 you can have at one time:</p>
<pre><code class="language-bash">❯ python aws_iam_self_service_key_rotation.py -c
New Key:        ****************QJLD
Secret:         ****************Epm2

Metadata response output:
{'AccessKey': {'UserName': 'jslabs', 'AccessKeyId': '****************QJLD', 'Status': 'Active', 'SecretAccessKey': '****************Epm2', 'CreateDate': datetime.datetime(2023, 4, 13, 20, 58, 23, tzinfo=tzutc())}, 'ResponseMetadata': {'RequestId': '12345678-79aa-4f83-a17e-cee94d4d51f2', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': '12345678-79aa-4f83-a17e-cee94d4d51f2', 'content-type': 'text/xml', 'content-length': '598', 'date': 'Thu, 13 Apr 2023 20:58:22 GMT'}, 'RetryAttempts': 0}} 

Update /home/jenna/.aws/credentials file w/ new key (y/n)? Warning: File contents will be overwritten! y

Updated /home/jenna/.aws/credentials file contents:
[default]
aws_access_key_id = ****************QJLD
aws_secret_access_key = ****************Epm2
</code></pre>
<p>At this point, you should update all of your applications to use the new key. Update your <code>~/.aws/credentials</code> file manually if needed or run <code>aws configure</code></p>
<p>Next, we'll update the status of the old key to inactive and check that the status has been updated:</p>
<pre><code class="language-bash">❯ python aws_iam_self_service_key_rotation.py -u ****************4NOP inactive
Metadata response output:
{'ResponseMetadata': {'RequestId': '12345678-5823-4038-be27-63b574b41359', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': '12345678-5823-4038-be27-63b574b41359', 'content-type': 'text/xml', 'content-length': '210', 'date': 'Thu, 13 Apr 2023 21:04:12 GMT'}, 'RetryAttempts': 0}}
❯ python aws_iam_self_service_key_rotation.py -l
Key:        ****************QJLD
Status:     Active
Created:    2023-04-13 20:58:23+00:00

Key:        ****************4NOP
Status:     Inactive
Created:    2023-02-19 14:51:31+00:00
</code></pre>
<p>Once we've validated our applications are working as expected with the new key, we can proceed with deleting the old/inactive key and validate we're left with the 1 active/in-use key:</p>
<pre><code class="language-bash">❯ python aws_iam_self_service_key_rotation.py -d ****************4NOP
****************4NOP has been deleted!

Metadata response output:
{'ResponseMetadata': {'RequestId': '12345678-21d2-4f42-b0ff-9e26d4ff5a02', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': '12345678-21d2-4f42-b0ff-9e26d4ff5a02', 'content-type': 'text/xml', 'content-length': '210', 'date': 'Thu, 13 Apr 2023 21:08:19 GMT'}, 'RetryAttempts': 0}}
❯ python aws_iam_self_service_key_rotation.py -l
Key:        ****************QJLD
Status:     Active
Created:    2023-04-13 20:58:23+00:00
</code></pre>
<h2>Conclusion</h2>
<p>You now have a self-service tool for your AWS IAM users to use for rotating their access keys on an as-needed basis. Although there are more automated approaches out there for AWS key rotation, I like the option of giving users some control over how and when to update their keys.</p>
<p>Hope you've found this useful! Let me know in the comments what you think and how this script could be improved. As an extension to this tool, I'm looking into creating a Lambda function that automatically expires access keys older than 90 days and includes an 📨 notification to users that their keys are expiring via SES so stay tuned for that!</p>
]]></content:encoded></item><item><title><![CDATA[Create GCP Compute Commitments Across a Project | Bash Script]]></title><description><![CDATA[Overview
Create GCP compute commitments across an entire project running a fleet of globally distributed resources using my latest bash script. The script is configured to purchase 12-month committed ]]></description><link>https://blog.jennasrunbooks.com/create-gcp-compute-commitments-across-a-project-bash-script</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/create-gcp-compute-commitments-across-a-project-bash-script</guid><category><![CDATA[google cloud]]></category><category><![CDATA[GCP]]></category><category><![CDATA[finops]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[Bash]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Mon, 03 Apr 2023 16:08:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/UZqq8Oi7PXk/upload/7a48cdd5de4fc5d40e84b5dd8c22a496.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Overview</h2>
<p>Create GCP compute commitments across an entire project running a fleet of globally distributed resources using my latest bash script. The script is configured to purchase 12-month committed resources based on multiple required input values including project-id, machine-type, commitment-type, # of vcpu's and amount of memory. Ideally, I would have preferred to implement this using Terraform as that is how this environment is managed. While the <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_reservation">google_compute_reservation</a> module is available, there is no module for compute commitments at the time of this writing which is what's required for this scenario. I didn't need to configure compute reservations so that capability will not be covered here.</p>
<h2>Script Functionality</h2>
<p><a href="https://github.com/jksprattler/gcp-tools/blob/main/scripts/gcp_committed_resources.sh"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680397882953/80dbef74-b2f7-4808-9148-fca27000fb91.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p><em>Link to script:</em> <a href="https://github.com/jksprattler/gcp-tools/blob/main/scripts/gcp_committed_resources.sh">gcp_committed_resources.sh</a></p>
<p>Based on the machine-type value entered  (i.e. <code>e2-standard-2</code>), the script will loop through all instances across your project running that machine type and create unique-named commitments for each instance using the following format: <code>&lt;machine-type&gt;-&lt;region&gt;-&lt;random#&gt;</code>,  example: <code>e2-standard-2-us-west4-19289</code>. This prevents duplicate commitment names in regions where you might be running multiple instances of the same machine type. The intent is to have each created commitment represent an individual VM's resources from within the residing region.</p>
<p>The script features a dry-run function that allows you to see the results of your input values and how they will be applied to the creation of commitments. Examples of potential conditionals against some inputs (i.e. Commitment Type, vcpu's and memory) can be uncommented and set for your use case. This can prevent allocating too many resources for a particular commitment whether this be a user input error (i.e. fat finger) or an oversight on what's being used by an existing machine type.</p>
<p>The Bash getopts built-in command parses options and enforces input arguments on these flags when a dry run or commitment creation is run: <code>-p</code> project-id, <code>-m</code> machine-type, <code>-t</code> commitment=type, <code>-v</code> vcpus, and <code>-M</code> memory. A dry run can be performed by running these options after passing any set conditionals. The changes will be applied and commitments created by running the dry run values in addition to appending the <code>-G</code> option which is set to <code>dry_run=0</code> resulting in the implementation of the <code>create_commitments</code> function.</p>
<p>Additional flags are available to assist with capturing details needed for the dry run and subsequent commitment creation. Here's the output of the usage function by invoking the <code>-h</code> (<code>--help</code> flag or null options can be applied for the same output):</p>
<pre><code class="language-bash">./gcp_committed_resources.sh -h
################################################################################
Option descriptions:
-p            Sets project-id
-m            Sets machine type of current running instances for commitments. Double check as gcloud filter is not exact match!
-t            Sets Commitment Type, for available options run -T
-v            Sets the amount of vcpu's for each committed resource per zone
-M            Sets the amount of Memory for each committed resource per zone, default is GB if not specified
-G            Caution! Creates the commitments. Always perform a dry run first without this option!
-P            Lists all existing project-id's to select from
-L            Lists all instances in set project and includes machine type, vm name &amp; zone
-T            Lists possible commitment type options, for details see: https://cloud.google.com/compute/docs/instances/signing-up-committed-use-discounts#commitment_types
-C            Lists current compute commitments
Usage:        [-p project-id] [-m machine-type] [-t commitment-type] [-v vcpus] [-M memory] [-G create-commitments]
Dry run ex.:  ./gcp_committed_resources.sh -p my-project -m e2-standard-2 -t general-purpose-e2 -v 2 -M 8
Creation ex.: ./gcp_committed_resources.sh -p my-project -m e2-standard-2 -t general-purpose-e2 -v 2 -M 8 -G
NOTE:         Check Quotas on Commitments against your regions w/ Limit: 0 vcpu's and submit requests for increases here or you'll encounter gcloud crashed (TypeError)
</code></pre>
<p>I've successfully run this script from one of the GCP projects in my environment, purchasing commitments for 40x globally distributed compute instances running two different machine types. Below are the details included in the pre-work analysis steps I performed and a guide with some example outputs from my implementation.</p>
<h2>Pre-Work Analysis</h2>
<h3>Cost</h3>
<p>According to Google Cloud <a href="https://cloud.google.com/compute/vm-instance-pricing#discounts">docs</a>:</p>
<blockquote>
<p>When you purchase vCPUs and/or memory on a 1-year commitment, you get the resources at a discount of 37% over the on-demand prices</p>
</blockquote>
<p>This is in alignment with the GCP <a href="https://cloud.google.com/products/calculator">calculator</a> estimates I ran in a cost savings analysis against my environment. Based on this, my company should see substantial enough savings 💰 for these 1-year commitments associated with 40x instances on two different machine types that are running consistently 24/7/365 to make this a worthwhile implementation.</p>
<p>I recommend performing a similar cost analysis exercise in your environment if you're pursuing purchasing compute commitments. If you're managing multiple GCP projects with a central billing account, then you might be interested in my blog post on performing a <a href="https://cloud.google.com/compute/docs/general-purpose-machines">GCP BigQuery Expression</a> to capture monthly invoice costs based on labels assigned to resources to assist in your cost analysis.</p>
<p>Google also has a <a href="https://cloud.google.com/compute/docs/instances/signing-up-committed-use-discounts#recommendations">Recommender</a> tool that evaluates the usage of your VM's over 30 days to determine whether or not they're eligible for committed use discounts (CUD) by checking the following:</p>
<ul>
<li><p>The VM was active for the entire duration of the 30 days.</p>
</li>
<li><p>The VM's SKU is part of an eligible committed use discount bucket.</p>
</li>
<li><p>The VM's usage was not already covered by an existing commitment.</p>
</li>
</ul>
<h3>Machine Type</h3>
<ol>
<li><p>Set your project-id: <code>./gcp_committed_resources.sh -p my-project</code></p>
</li>
<li><p>Determine the machine type used by your compute instances in a specified project by running the script with the <code>-L</code> flag.</p>
<ul>
<li><p>Get all machines sorted by machine type:  <code>./gcp_committed_resources.sh -L</code></p>
</li>
<li><p>Grep for a list of specific machine types: <code>./gcp_committed_resources.sh -L | grep e2-standard-2</code></p>
</li>
</ul>
</li>
<li><p>Once you have the machine types listed that you want to purchase committed resources for, cross-check them against the GCP list of <a href="https://cloud.google.com/compute/docs/instances/signing-up-committed-use-discounts#purchasecommitment">commitment types</a> that are eligible for commitments.</p>
</li>
<li><p>If your machine type is eligible for commitments, then review the page associated with your specific compute engine family to capture the # of vpcu's and amount of memory for that machine type. For example, the <code>e2-standard-2</code> are in the General purpose machines <a href="https://cloud.google.com/compute/docs/general-purpose-machines">doc</a> with the following specs:</p>
</li>
</ol>
<table>
<thead>
<tr>
<th>Machine types</th>
<th>vCPUs*</th>
<th>Memory (GB)</th>
<th>Max number of persistent disks (PDs)†</th>
<th>Max total PD size (TB)</th>
<th>Local SSD</th>
<th>Maximum egress bandwidth (Gbps)‡</th>
</tr>
</thead>
<tbody><tr>
<td><code>e2-standard-2</code></td>
<td>2</td>
<td>8</td>
<td>128</td>
<td>257</td>
<td>No</td>
<td>4</td>
</tr>
</tbody></table>
<p>Note: If the majority of your compute instances are running the same machine type in the same region, then this script may not be well suited for your use case. In that scenario, I would probably calculate the total vcpu's and memory needed for a particular commitment type and create a single commitment in that region running a the <code>gcloud compute commitments create</code> command once.</p>
<h3>Commitment &amp; CPU Quota Limits</h3>
<p>Before proceeding with creating the new commitments, I highly recommend reviewing both the Commitment and CPU Quota limits on the regions you're working with. I didn't do this step during my pre-work and was affected by a generic gcloud error that broke my while loop while running the script against my second set of machine types which looked like this:</p>
<pre><code class="language-bash">###########################################
Commitment Name:  e2-standard-2-asia-south2-22948
ERROR: gcloud crashed (TypeError): expected string or bytes-like object
		If you would like to report this issue, please run the following command:
  gcloud feedback
		To check gcloud for common problems, please run the following command:
  gcloud info --run-diagnostics
</code></pre>
<p>Searching a few posts online led me to discover it was a commitment quota limit affecting a handful of regions in my deployment. Here are a couple references to Google docs that can help you avoid the gcloud crash that I ran into:</p>
<ul>
<li><p>View your quota in the Google Cloud console <a href="https://cloud.google.com/docs/quota#viewing_your_quota_console">doc</a></p>
</li>
<li><p>Resource usage quotas and permission management <a href="https://cloud.google.com/compute/resource-usage">doc</a></p>
</li>
</ul>
<p>Cross-reference the <code>./gcp_committed_resources.sh -L</code> captured in the Machine Type section against the quota limits set in your regions. If needed, submit a request for quota increases that will cover the number of commitments and/or resources (ie CPU) you'll be creating for that region. If you have multiple instances of the same machine type in the same region, be sure to request quota increases to reflect that.</p>
<p>Here's an example of what a commitment quota limit increase response from Google looks like after submitting the request using the cloud console:</p>
<pre><code class="language-bash">Your quota request for my-project has been approved and your project quota has been adjusted according to the following requested limits:
		
		+-------------+--------------------------------+-------------------------+-----------------+----------------+
		| NAME        | DIMENSIONS                     | REGION                  | REQUESTED LIMIT | APPROVED LIMIT |
		+-------------+--------------------------------+-------------------------+-----------------+----------------+
		| COMMITMENTS | region=asia-south2             | asia-south2             | 2               | 2              |
		|             |                                |                         |                 |                |
		| COMMITMENTS | region=europe-central2         | europe-central2         | 2               | 2              |
		|             |                                |                         |                 |                |
		| COMMITMENTS | region=northamerica-northeast2 | northamerica-northeast2 | 2               | 2              |
		|             |                                |                         |                 |                |
		| COMMITMENTS | region=southamerica-west1      | southamerica-west1      | 2               | 2              |
		+-------------+--------------------------------+-------------------------+-----------------+----------------+
		
After approved, Quotas can take up to 15 min to be fully visible in the Cloud Console and available to you.
</code></pre>
<p>It took about 20 minutes for the change to appear for me in the Quota service of the cloud console.</p>
<p>Here's a screenshot of what it looked like after the quota limits were increased and I had successfully created commitments for those affected regions:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680532985766/172c36f9-55f2-40c1-ba3f-cfa0564187fc.png" alt="" style="display:block;margin:0 auto" />

<h2>Implementation</h2>
<p>Purchasing commitments without attached reservations is the Google <a href="https://cloud.google.com/compute/docs/instances/signing-up-committed-use-discounts#purchasecommitment">doc</a> I followed for my scenario which includes the required permissions for applying these changes.</p>
<ol>
<li><p>Ensure to run all the "Lists" options defined in the Usage as part of your pre-work analysis before the implementation. These should be used to help build out your dry run results followed by the actual implementation with the <code>-G</code> flag set.</p>
</li>
<li><p>Perform a dry run with the following options set: <code>[-p project-id] [-m machine-type] [-t commitment-type] [-v vcpus] [-M memory]</code></p>
<ul>
<li><p>Example dry run:</p>
<pre><code class="language-bash">❯ ./gcp_committed_resources.sh -p my-project -m e2-standard-2 -t general-purpose-e2 -v 2 -M 8
Updated property [core/project].
Project set to: my-project
###########################################
Commitment Name:  e2-standard-2-asia-east1-6065
Region:           asia-east1
Project:          my-project
Resources:        vcpu=2,memory=8
Plan:             12-month
Type:             general-purpose-e2
###########################################
</code></pre>
</li>
</ul>
</li>
<li><p>Perform the implementation of the create commitments function by appending the <code>-G</code> flag to your dry run options: <code>[-p project-id] [-m machine-type] [-t commitment-type] [-v vcpus] [-M memory] [-G create-commitments]</code></p>
<ul>
<li><p>Example successful commitment creation run:</p>
<pre><code class="language-bash">❯ ./gcp_committed_resources.sh -p my-project -m e2-standard-2 -t general-purpose-e2 -v 2 -M 8 -G
Updated property [core/project].
Project set to: my-project
###########################################
Commitment Name:  e2-standard-2-asia-east1-28472
Created [https://www.googleapis.com/compute/v1/projects/my-project/regions/asia-east1/commitments/e2-standard-2-asia-east1-28472].
---
autoRenew: false
category: MACHINE
creationTimestamp: '2023-03-31T07:56:32.947-07:00'
endTimestamp: '2024-04-01T00:00:00.000-07:00'
id: 'my-id'
kind: compute#commitment
name: e2-standard-2-asia-east1-28472
plan: TWELVE_MONTH
region: https://www.googleapis.com/compute/v1/projects/my-project/regions/asia-east1
resources:
- amount: '2'
  type: VCPU
- amount: '8192'
  type: MEMORY
selfLink: https://www.googleapis.com/compute/v1/projects/my-project/regions/asia-east1/commitments/e2-standard-2-asia-east1-28472
startTimestamp: '2023-04-01T00:00:00.000-07:00'
status: NOT_YET_ACTIVE
statusMessage: The commitment is not yet active (its startTimestamp is in the future).
  It will not apply to current resource usage.
type: GENERAL_PURPOSE_E2
###########################################
</code></pre>
</li>
</ul>
</li>
<li><p>As soon as the committed resources have been purchased they'll display as "Pending" in the cloud portal and "NOT_YET_ACTIVE" from the gcloud cli.</p>
<ul>
<li><p>Example output of current commitments list using <code>-C</code> flag immediately post-change:</p>
<pre><code class="language-bash">❯ ./gcp_committed_resources.sh -C
NAME                                        REGION                   END_TIMESTAMP                  STATUS
e2-standard-2-us-central1-7087              us-central1              2024-04-01T00:00:00.000-07:00  NOT_YET_ACTIVE
e2-standard-2-europe-west1-13832            europe-west1             2024-04-01T00:00:00.000-07:00  NOT_YET_ACTIVE
e2-standard-2-us-west1-21069                us-west1                 2024-04-01T00:00:00.000-07:00  NOT_YET_ACTIVE
e2-highcpu-8-asia-east1-14956               asia-east1               2024-04-01T00:00:00.000-07:00  NOT_YET_ACTIVE
e2-highcpu-8-asia-east1-20966               asia-east1               2024-04-01T00:00:00.000-07:00  NOT_YET_ACTIVE
e2-standard-2-asia-east1-28472              asia-east1               2024-04-01T00:00:00.000-07:00  NOT_YET_ACTIVE
</code></pre>
</li>
<li><p>Commitments will move into an Active state at midnight the following day per Google <a href="https://cloud.google.com/compute/docs/instances/signing-up-committed-use-discounts#purchasecommitment">doc</a>:</p>
<blockquote>
<p>After purchasing a commitment, the commitment is effective starting at midnight the following day. For example, a commitment purchased on Monday afternoon at 3 PM US and Canadian Pacific Time (UTC-8, or UTC-7 during daylight saving time) becomes effective on Tuesday at 12 AM US and Canadian Pacific Time (UTC-8 or UTC-7). The discounts are automatically applied to applicable instances in the region you specified, and to the projects in which those discounts are purchased.</p>
</blockquote>
</li>
<li><p>Output using <code>-C</code> flag 24+ hours post-change:</p>
<pre><code class="language-bash">❯ ./gcp_committed_resources.sh -C
NAME                                        REGION                   END_TIMESTAMP                  STATUS
e2-standard-2-us-central1-7087              us-central1              2024-04-01T00:00:00.000-07:00  ACTIVE
e2-standard-2-europe-west1-13832            europe-west1             2024-04-01T00:00:00.000-07:00  ACTIVE
e2-standard-2-us-west1-21069                us-west1                 2024-04-01T00:00:00.000-07:00  ACTIVE
e2-highcpu-8-asia-east1-14956               asia-east1               2024-04-01T00:00:00.000-07:00  ACTIVE
e2-highcpu-8-asia-east1-20966               asia-east1               2024-04-01T00:00:00.000-07:00  ACTIVE
e2-standard-2-asia-east1-28472              asia-east1               2024-04-01T00:00:00.000-07:00  ACTIVE
</code></pre>
</li>
</ul>
</li>
<li><p>If you happen to run into a gcloud error such as the one referenced in the Commitment &amp; CPU Quota Limits section above, you won't be able to re-run the script as you'll end up creating duplicate commitments for resources you've already purchased commitments for creating unintended additional cloud spend 💸 I've created another script for these scenarios with <code>_manual</code> appended to the name here: <a href="https://github.com/jksprattler/gcp-tools/blob/main/scripts/gcp_committed_resources_manual.sh">gcp_committed_resources_manual.sh</a></p>
<ul>
<li><p>The script performs a one-time loop through an array of defined regions that missed getting commitments created.</p>
</li>
<li><p>The region array/list, PROJECT_ID, vcpu, memory, and commitment type all need to be hard coded for your use case.</p>
</li>
<li><p>If you have duplicate machine types hosted in any of the regions listed in the array, you'll need to account for that. You can rerun the script to create commitments in those scenarios since commitment names are appended with the <code>$RANDOM</code> internal Bash function.</p>
</li>
<li><p>Perform a dry run with: <code>./gcp_committed_resources_manual.sh</code></p>
</li>
<li><p>Perform the implementation with: <code>./gcp_committed_resources_manual.sh --go</code></p>
</li>
</ul>
</li>
</ol>
<p>❗️❗️Warning ❗️❗️ Extra caution should be taken using these scripts as you cannot back out of purchasing committed resources. When creating GCP commitments you are agreeing to pay for the resources assigned to your commitments under the length of time defined in the plan, in this case, 12 months.</p>
<h2>Conclusion</h2>
<p>I've successfully run these scripts from one of the GCP projects in my environment, purchasing commitments for 40x globally distributed compute instances running two different machine types. This should result in a discount of 37% over on-demand pricing resulting in worthwhile savings on resources running in the project.</p>
<p>While running the script to create commitments for the 2nd set of machine types I encountered the <code>gcloud error</code> as mentioned in the Commitment &amp; CPU Quota Limits section above. I was able to successfully create commitments for these remaining regions using the secondary Manual script referenced in Step 5. of the Implementation section. To avoid this in the future, I'll be taking a closer look at Quota limits for my regions so I can run the primary script once through.</p>
<p>At the expiration of a commitment plan, you could renew your commitments with a simple bash script that loops through all the existing commitments capturing the Commitment Name and associated region using the <code>gcloud compute commitments list</code> command or using the <code>-L</code> flag in the script and apply these values to the <code>gcloud compute commitments update $COMMITMENT_NAME --auto-renew --region=$region</code> command referenced <a href="https://cloud.google.com/sdk/gcloud/reference/compute/commitments/update">here</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Azure AD & RBAC with Terraform Part 2]]></title><description><![CDATA[This article was originally published in January 2023 on my GitHub io blog here

Overview
After publishing my initial runbook exploring this topic, I decided to test out and implement the HashiCorp fo]]></description><link>https://blog.jennasrunbooks.com/azure-ad-rbac-with-terraform-part-2</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/azure-ad-rbac-with-terraform-part-2</guid><category><![CDATA[Terraform]]></category><category><![CDATA[Azure]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[cloud security]]></category><category><![CDATA[rbac]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Sat, 25 Mar 2023 01:31:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/maaWpQVgi00/upload/dc548d8a6c9110ca6e9fda6e88c38186.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em><strong>This article was originally published in January 2023 on my GitHub io blog</strong></em> <a href="https://jksprattler.github.io/jennas-runbooks/Azure/bazure-tf-ad-rbac-pt2.html"><em><strong>here</strong></em></a></p>
</blockquote>
<h2><strong>Overview</strong></h2>
<p>After publishing my <a href="https://jksprattler.github.io/jennas-runbooks/Azure/azure-tf-ad-rbac.html">initial runbook</a> exploring this topic, I decided to test out and implement the <a href="https://developer.hashicorp.com/terraform/tutorials/azure/azure-ad">HashiCorp</a> <code>for_each</code> meta-argument method for managing the Azure AD User base of a production environment I'm currently working with. I wanted to share my findings from that experience here. In this 2nd part of my series on Azure AD &amp; RBAC with Terraform, I define the requirements necessary for setting up Azure AD User and Group administration using this alternative method. I've also highlighted some tips from issues I ran into during my implementation of this and a Validation section containing helpful commands for post-checkouts and troubleshooting. While the initial runbook and demo in this series still have useful information such as a deep dive into the security behind Azure AD and RBAC and some test login scenarios, I've found this CSV file method to be much more efficient at managing users and group membership. Also, I've refined the security privileges around the GitHub Actions SPN and have implemented the creation and management of the SPN, including its API permissions, using Terraform resource blocks.</p>
<h2>Topics Covered</h2>
<ul>
<li><p>GH Actions SPN creation and permission configurations are done in Terraform</p>
</li>
<li><p>GH Actions SPN gets further locked down with reduced privileges from Subscription Owner to Reader and from User Admin to specific API Reader level permissions</p>
</li>
<li><p>Azure AD Users managed by a Terraform for_each meta-argument using data populated into a CSV file</p>
</li>
<li><p>Azure AD Group members assigned using Terraform for_each meta-argument against users assigned to a specific Department</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679707358643/80b9d7e8-aa28-41a4-bfed-7cd9d52cadef.png" alt="" style="display:block;margin:0 auto" />

<h2>Pre-requisites</h2>
<ul>
<li><p>See Part 1 runbook: <a href="https://blog.jennasrunbooks.com/azure-ad-rbac-with-terraform-part-1">Azure AD &amp; RBAC with Terraform</a></p>
</li>
<li><p>Ensure Security defaults are disabled to enable the creation of a Conditional Access policy.</p>
<ul>
<li>From the Portal UI navigate to Azure AD &gt; properties &gt; manage security defaults &gt; enable security defaults toggle to no.</li>
</ul>
</li>
<li><p>Not required: if you're interested in how I got the highlighted changes in my GH Actions bot PR comments see my runbook: <a href="https://blog.jennasrunbooks.com/silence-refreshing-state-highlight-changes-in-github-actions-terraform-plan-output">Silence "Refreshing state…" &amp; Highlight Changes in Github Actions Terraform Plan Output</a></p>
</li>
</ul>
<h2>Design Artifacts</h2>
<ul>
<li><p>YouTube <a href="https://youtu.be/cIqvYL9burQ">demo recording</a></p>
</li>
<li><p>Clone/Fork the repo containing the Terraform artifacts for the Azure AD config here: <a href="https://github.com/jksprattler/azure-security">jksprattler/azure-security</a></p>
<ul>
<li>Relevant files are under <code>/azuread-users-groups-roles-pt2</code></li>
</ul>
</li>
<li><p>Config for the GH Actions SPN, storage account and blob container hosting the terraform backend state files is under <code>/azure-dev-infra</code></p>
</li>
</ul>
<h2>Procedure</h2>
<ol>
<li><p>Authenticate to Azure and configure local environment variables to run terraform commands from your local terminal:</p>
<pre><code class="language-bash">az login
export ARM_SUBSCRIPTION_ID=$(az account show --query id | xargs)
export ARM_ACCESS_KEY="&lt;insert storage account access key used for backend state configs&gt;"
</code></pre>
<p>For enterprise environments with multiple subscriptions, run the following to get the specific SubID you need: <code>az account list --query '[].{SubID:id, SubName:name}' -o table</code></p>
</li>
<li><p>Create the <code>gh-actions-runbooks-ad</code> SPN and its required API permissions configured in the <a href="http://apps.tf"><code>apps.tf</code></a> file under the <code>azure-dev-infra</code> directory. This will be used to perform the terraform plan output in the GitHub PR comments. This config resides under this directory since the SPN is required for all other infrastructure plan outputs in the other directories of the repo.</p>
</li>
<li><p>Once created, apply the "Grant admin consent for Default Directory" to the API permissions for the SPN by running the az CLI command: <code>az ad app permission admin-consent --id &lt;app_id value from outputs&gt;</code></p>
</li>
<li><p>Assign the SPN outputs as GitHub Secrets in your repo using the following variables:</p>
<pre><code class="language-bash">ARM_SUBSCRIPTION_ID="&lt;sub_id&gt;"
ARM_TENANT_ID="&lt;tenant_id&gt;"
ARM_CLIENT_ID="&lt;app_id&gt;"
ARM_CLIENT_SECRET="&lt;auth_client_secret&gt;"
</code></pre>
<details>
<summary>Tip</summary>
<p>In the previous runbook, step 4. mentioned assigning the SPN the User Admin AD role however, I found this is not required with the API Permissions configured to allow AD User, Group and Domain Reader access. The Subscription Owner RBAC role is not needed since we can assign the Subscription Reader RBAC role as we just need it to be allowed to read the data during the terraform plan.</p>
</details></li>
<li><p>Populate the <code>users.csv</code> file with users. If you're importing an existing Azure AD user base into Terraform, navigate in the Portal UI to Azure AD &gt; Users : Download users to capture a csv file of existing users. Extract the user data from the existing CSV file and populate the <code>users.csv</code> Terraform file with the required fields: <code>first_name,last_name,mail_nickname,preferred_language</code></p>
<details>
<summary>Tip</summary>
<p>The usage_location value is required for users that are assigned Microsoft licenses such as O365</p>
</details></li>
<li><p>Assign the users to a <code>department</code> such as Art or Engineering if you'd like to auto-assign them to the Azure AD Groups created in this lab. The Art Group uses Dynamic Membership to assign users requiring the Azure AD Premium P1 license. The Art group also includes a Conditional Access policy assignment enforcing MFA into the portal for all users in the group. The Engineering group resource block includes a for_each argument which loops through all users and for each user assigned to the Engineering department it assigns their membership to the Engineering group. The Engineering group does not require any Azure AD Premium licenses and is a nice option for automating group membership when you want to keep costs down and don't require the use of conditional access policies.</p>
</li>
<li><p>Save and commit the changes. Review the <code>github-actions</code> bot output in your PR comments which uses the <code>gh-actions-runbooks-ad</code> SPN created in Step 2. Make adjustments to your code as needed.</p>
</li>
<li><p>Perform a manual terraform apply from your local directory.</p>
<details>
<summary>Tip</summary>
<p>Terraform will assign each Azure AD user resource a unique ID using the <code>mail_nickname</code> value set in the <code>users.csv</code> file. This identifier can be used for assigning Azure AD group owner/membership. User Principal Names are used for the Azure Portal login username and will require a password reset upon initial login. The new user will be created with an auto-generated password using the following pattern in all lowercase letters in a single string: <code>lastname + first letter of first name + numerical value for length of first name + !123 </code>I added chars <code>123</code> as I found the Microsoft password length requirement was not met on users with last names less than 5 chars long. This should fix that issue.</p>
</details></li>
<li><p>To delete users, remove the entire line entry for that user from the <code>users.csv</code> file and be sure to remove any user identifiers (i.e., <code>azuread_user.users["userarose"]</code>) manually assigned to Azure AD groups.</p>
</li>
</ol>
<h2>Validations</h2>
<ul>
<li><p>List resources managed by Terraform: <code>terraform state list</code></p>
</li>
<li><p>Show AD user info: <code>terraform state show 'azuread_user.users["userarose"]'</code></p>
</li>
<li><p>List all Azure AD users: <code>az ad user list --query "[].{name:displayName,userPrincipalName:userPrincipalName, ObjectID:id}" -o tsv</code></p>
</li>
<li><p>List the 2 groups that were created: <code>az ad group list --query "[?contains(displayName,'Engineering')].{ name: displayName }" -o tsv</code></p>
</li>
<li><p>List the users in the groups: <code>az ad group member list --group "Engineering" --query "[].{ name: displayName }" -o tsv</code></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>You now have a much more efficient method for managing Azure AD Users using the CSV file and Terraform <code>for_each</code> meta-arguments. Azure AD Group membership is dynamically configured using <code>for_each</code> meta-arguments against department names assigned to users which excludes the requirement for purchasing Azure AD Premium P1 licenses. The SPN used by the GitHub Actions workflow is further locked down to adhere to the principal of least privileges and it's configured in the Terraform code. Having your AD users, groups and SPN's configured as code allows for consistency of settings and permissions across your infrastructure. It also increases a level of security awareness since PR's will require review/approval for code changes.</p>
]]></content:encoded></item><item><title><![CDATA[GCP BigQuery Expression | Monthly Invoice Based on Labels]]></title><description><![CDATA[In this GCP BigQuery expression, I show how you can retrieve the monthly invoice data on all resources in a specified project based on assigned label values. This method of capturing cloud spend can b]]></description><link>https://blog.jennasrunbooks.com/gcp-bigquery-expression-monthly-invoice-based-on-labels</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/gcp-bigquery-expression-monthly-invoice-based-on-labels</guid><category><![CDATA[google cloud]]></category><category><![CDATA[GCP]]></category><category><![CDATA[bigquery]]></category><category><![CDATA[finops]]></category><category><![CDATA[data analysis]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Tue, 14 Mar 2023 13:10:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/nApaSgkzaxg/upload/319b78f1215dc86fed881b2eb7ec1178.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this GCP BigQuery expression, I show how you can retrieve the monthly invoice data on all resources in a specified project based on assigned label values. This method of capturing cloud spend can be applied after you've enabled the <a href="https://cloud.google.com/billing/docs/how-to/export-data-bigquery">Cloud Billing data export to BigQuery</a> functionality.</p>
<p>This query performs the following functions:</p>
<ul>
<li><p>The sum of regular costs, taxes, adjustments and rounding errors for each invoice month using inner CAST</p>
</li>
<li><p>The CROSS JOIN statement excludes Null arrays/rows and UNNEST operator flattens the array of labels into rows</p>
</li>
<li><p>Filter by project name</p>
</li>
<li><p>Group by key/value labels and invoice month</p>
</li>
<li><p>Order by invoice month</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678737413340/05d5abdb-9eec-4719-9226-4588e4c1a99c.png" alt="" style="display:block;margin:0 auto" />

<p>This is just one scenario displaying the value of applying tags/labels on your cloud resources. I've found tagging essential when it comes to cloud cost analysis, especially across a multi-cloud, multi-platform infrastructure!</p>
<p>Link to query on Gist: <a href="https://gist.github.com/jksprattler/80519ade571714c4415e103ccc8ad2be">https://gist.github.com/jksprattler/80519ade571714c4415e103ccc8ad2be</a></p>
]]></content:encoded></item><item><title><![CDATA[Bash Script to Enable VPC Flow Logs on all subnets in a GCP Project]]></title><description><![CDATA[Here's a super simple bash script to enable VPC flow logs on every subnet across a specified GCP project with an option to include or exclude logging metadata that I wrote. It has the aggregation inte]]></description><link>https://blog.jennasrunbooks.com/bash-script-to-enable-vpc-flow-logs-on-all-subnets-in-a-gcp-project</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/bash-script-to-enable-vpc-flow-logs-on-all-subnets-in-a-gcp-project</guid><category><![CDATA[google cloud]]></category><category><![CDATA[vpc]]></category><category><![CDATA[Bash]]></category><category><![CDATA[networking]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Wed, 08 Mar 2023 16:01:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/12ea-y_1-UE/upload/1cf1140af47de31f8efb9f42cf3af889.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here's a super simple bash script to enable VPC flow logs on every subnet across a specified GCP project with an option to include or exclude logging metadata that I wrote. It has the aggregation interval set to 10-min however, you could modify this to your needs in addition to setting additional flags available for the VPC flow log <a href="https://cloud.google.com/vpc/docs/using-flow-logs#gcloud">settings</a>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678290465894/1f3ba84b-dfe3-43ba-a60b-e1e4343e6bd3.png" alt="" style="display:block;margin:0 auto" />

<p>Here are some <a href="https://cloud.google.com/vpc/docs/flow-logs#use_cases">use cases</a> for enabling VPC flow logs:</p>
<ul>
<li><p>Network monitoring: diagnostics and capacity forecasting</p>
</li>
<li><p>Network traffic optimization expenses: inter-region and zone traffic analysis</p>
</li>
<li><p>Network forensics: examine compromised IPs</p>
</li>
<li><p>Real-time security analysis: SIEM integration</p>
</li>
</ul>
<p>Link to script: <a href="https://github.com/jksprattler/gcp-networking/blob/main/scripts/enable-flowlogs.sh">https://github.com/jksprattler/gcp-networking/blob/main/scripts/enable-flowlogs.sh</a></p>
]]></content:encoded></item><item><title><![CDATA[Azure AD & RBAC with Terraform Part 1]]></title><description><![CDATA[This article was originally published in September 2022 on my GitHub io blog here

Overview
The purpose of this runbook is to demonstrate a potential approach to managing Azure AD users, groups and Ro]]></description><link>https://blog.jennasrunbooks.com/azure-ad-rbac-with-terraform-part-1</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/azure-ad-rbac-with-terraform-part-1</guid><category><![CDATA[Terraform]]></category><category><![CDATA[Azure]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[cloud security]]></category><category><![CDATA[Cloud]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Wed, 08 Mar 2023 14:51:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/D44kHt8Ex14/upload/2488448bc2445a4f85a4eb0374e12cb9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em><strong>This article was originally published in September 2022 on my GitHub io blog</strong></em> <a href="https://jksprattler.github.io/jennas-runbooks/Azure/azure-tf-ad-rbac.html"><em><strong>here</strong></em></a></p>
</blockquote>
<h2>Overview</h2>
<p>The purpose of this runbook is to demonstrate a potential approach to managing Azure AD users, groups and Role-Based Access Control (RBAC) by following Terraform's declarative model with automated checkouts using GitHub Actions CI/CD Workflows. Both the <a href="https://registry.terraform.io/providers/hashicorp/azuread/latest/docs">Azure AD</a> and <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs">Azure RM</a> Terraform providers will be used to implement Identity &amp; Access Management as code which will allow for automated provisioning of users, groups, custom role definitions, role assignments and conditional access policies. The principle of least privilege will be followed in the delegation of both Azure AD and Azure RBAC assignments. The procedure will use a combination of AZ CLI, Terraform, python scripts and Azure Portal UI. Users will be assigned to AD Groups dynamically based on their department (ie Art, Engineering). A conditional access policy will be created to demonstrate Multi-factor authentication (MFA) enforcement based on dynamic group assignment. An Azure Service Principal (SPN) will be used to perform the automated GitHub Actions workflow jobs based on code commits generating terraform checks and plan output. Finally, Azure AD self-service password reset (SSPR) will be enabled for all users.</p>
<h3><strong>Azure AD &amp; RBAC Topics Covered</strong></h3>
<ul>
<li><p>Azure AD identity governance of users and groups (dynamic)</p>
</li>
<li><p>Azure RBAC group assignment using both built-in and custom roles</p>
</li>
<li><p>Azure RBAC assignments scoped to dynamic Azure AD groups</p>
</li>
<li><p>Azure AD Conditional Access Policy enforcing Multi-factor authentication (MFA) for Users based on dynamic Group membership</p>
</li>
<li><p>Azure Service Principal (SPN) to provision terraform configuration via GitHub Actions automation</p>
</li>
<li><p>Azure AD self-service password reset (SSPR)</p>
</li>
</ul>
<h3><strong>Pre-requisites</strong></h3>
<ul>
<li><p>GitHub Account</p>
</li>
<li><p>Microsoft Account</p>
</li>
<li><p>Azure Subscription</p>
</li>
<li><p>Azure AD Premium P1/P2 license required for configuring: Conditional Access policy, MFA with conditional access, Dynamic groups</p>
<ul>
<li>A free trial can be activated from within your Azure AD tenant for a limited time</li>
</ul>
</li>
<li><p>Azure <a href="https://docs.microsoft.com/en-us/cli/azure/">CLI</a> installed and credentials for <a href="https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli">authentication</a></p>
</li>
<li><p>Terraform <a href="https://learn.hashicorp.com/tutorials/terraform/install-cli">installed</a></p>
</li>
<li><p>Azure RG, Storage account and blob container setup if you choose to maintain terraform state files remotely.</p>
</li>
</ul>
<h3><strong>Design Artifacts</strong></h3>
<ul>
<li><p>YouTube <a href="https://youtu.be/nVf5pYGeNTc">demo recording</a></p>
</li>
<li><p>Clone/Fork the repo containing the Terraform artifacts for the Azure AD config here: <a href="https://github.com/jksprattler/azure-security">jksprattler/azure-security</a></p>
<ul>
<li><p>Relevant files are under <code>/azuread-users-groups-roles</code></p>
</li>
<li><p>Config for the storage account and blob container hosting the terraform backend state files is under <code>/azure-dev-infra</code></p>
</li>
</ul>
</li>
</ul>
<h2>Diagram</h2>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678285735410/42b90549-eef4-4051-b516-c0725451e8ea.png" alt="" style="display:block;margin:0 auto" />

<h2>Procedure</h2>
<ol>
<li><p>Authenticate to Azure and configure local environment variables to run terraform commands from your local terminal:</p>
<pre><code class="language-bash">az login
export ARM_SUBSCRIPTION_ID=$(az account show --query id | xargs)
export ARM_ACCESS_KEY="&lt;insert storage account access key used for backend state configs&gt;"
</code></pre>
</li>
<li><p>Create an Azure SPN assigning the RBAC role of owner at the subscription level so it has privileges to both manage all resources and assign RBAC roles to other users. This identity will be used for provisioning the terraform configuration using the GH Actions workflows:</p>
<pre><code class="language-bash">az ad sp create-for-rbac --name "gh-actions-runbooks-ad" --role owner \
                      --scopes /subscriptions/{subscription-id} \
                      --sdk-auth                        
# Replace {subscription-id} with the subscription details
# The command should output a JSON object similar to this:
{
  "clientId": "&lt;GUID&gt;",
  "clientSecret": "&lt;GUID&gt;",
  "subscriptionId": "&lt;GUID&gt;",
  "tenantId": "&lt;GUID&gt;",
  (...)
}
</code></pre>
</li>
<li><p>Assign the output of the JSON objects as GitHub Secrets in your repo using the following variables:</p>
<pre><code class="language-bash">ARM_SUBSCRIPTION_ID="&lt;azure_subscription_id&gt;"
ARM_TENANT_ID="&lt;azure_subscription_tenant_id&gt;"
ARM_CLIENT_ID="&lt;service_principal_appid&gt;"
ARM_CLIENT_SECRET="&lt;service_principal_password&gt;"
</code></pre>
</li>
<li><p>Assign the User administrator Azure AD role to the SPN. From Portal UI navigate to Azure AD &gt; Roles and Administrators blade &gt; "User Administrator" Role &gt; Add Assignments &gt; Select members &gt; Filter by service principal display name.</p>
<details>
<summary>As of this writing I couldn’t find an efficient CLI method for applying Azure AD roles to SPN’s as Azure CLI is unsupported and Powershell cmdlets, which are still in preview mode, gave errors leaving the portal as the best option</summary>
<p></p>
</details></li>
<li><p>Assign the API permissions from the below screenshot to the SPN to allow read/write access to the conditional access policy. From Portal UI navigate to App registrations &gt; locate and select your SPN &gt; API permissions: Add permission and be sure to select "Grant admin consent for Default Directory" once all the Application type API permissions have been added.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678285897743/4413042d-55d5-47ec-82df-085a1c30d41e.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Disable Security defaults to enable the creation of a Conditional Access policy. From the Portal UI navigate to Azure AD &gt; properties &gt; manage security defaults &gt; enable security defaults toggle to no.</p>
</li>
<li><p>Create a new Azure AD user by making a new local branch from the cloned <a href="https://github.com/jksprattler/azure-security">jksprattler/azure-security</a> repo. Run the <a href="http://azuread-create-users.py"><code>azuread-create-users.py</code></a> script to generate the terraform syntax for your new user: <code>python scripts/azuread-create-users.py</code></p>
<ul>
<li>The script will run an az cli command invoking a custom request through Microsoft Graph to capture your Azure AD default domain using your current az login.</li>
</ul>
</li>
<li><p>Navigate to the <code>azuread-users-groups-roles</code> directory and paste the terraform code for your new user into the <a href="http://main.tf"><code>main.tf</code></a> file using either of the existing Engineering or Art AD Groups or create a new group. For example:</p>
<pre><code class="language-bash">resource "azuread_user" "raybrown" {
  user_principal_name   = "raybrown@jennasrunbooks.com"
  display_name          = "Ray Brown"
  department            = "Art"
  password              = "Super$ecret01@!"
  force_password_change = true
}
</code></pre>
</li>
<li><p>Save and commit the changes. Review the <code>github-actions</code> bot output from your PR, specifically the terraform plan results, which will perform the following functions on your behalf using the Azure SPN (ie <code>gh-actions-runbooks-ad</code>):</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678286512480/e9949e06-ce6c-4e0a-8427-aebff1cff98d.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>From the <code>azuread-users-groups-roles</code> directory, perform your <code>terraform apply</code> to create the new Azure AD user.</p>
</li>
<li><p>Enable Self Service Password Reset (SSPR) for All users. From Portal UI navigate to: Azure AD &gt; Password reset &gt; Auth methods &gt; SSPR enabled: All</p>
</li>
</ol>
<h4><strong>Import Existing Azure AD Users into Terraform</strong></h4>
<p>If you have a number of users that already exist in your Azure AD and are looking to start managing this part of your cloud estate using Terraform, you can run the <code>scripts/azuread-import-users.py</code> script which will extract a list of your current Azure AD user's Display Names, Principal Names and Departments associated with the current Azure tenant you are logged into (<code>az login</code>). The script runs an az ad query capturing the user details and copies them to a tsv file which is then read by python and converted into Terraform syntax. Once you have your list of users, follow the Procedure above starting at step 8.</p>
<pre><code class="language-bash">python scripts/azuread-import-users.py
# For much larger lists of users, save the python output to either a txt or tf file
python scripts/azuread-import-users.py &gt; users.tf
</code></pre>
<h2>Summary</h2>
<p>Upon completion of the above procedure, you should now have a basic architecture started for implementing Azure IAM as code using Azure AD and Azure RM Terraform providers. A CI/CD pipeline is implemented using a GitHub Actions workflow generating Terraform format/init/validate/plan results based on PR commits providing a solution for code review by your team before invoking <code>terraform apply</code> locally. Python scripts are available for generating terraform syntax for new Azure AD users (<a href="http://azuread-create-users.py"><code>azuread-create-users.py</code></a> ) and for extracting a list of existing Azure AD users which then generates the terraform code for the list of existing users to be imported into your terraform state (<a href="http://azuread-import-users.py"><code>azuread-import-users.py</code></a>). While this approach might be a good initial start for managing your Azure IAM as code, alternatively there's a link in the references section below to another more elegant solution to try provided by HashiCorp which uses a for_each loop against a CSV file of users. Check out my <a href="https://blog.jennasrunbooks.com/azure-ad-rbac-with-terraform-part-2">Part 2 post</a> in this series where I apply the <code>for_each</code> meta-argument for this implementation.</p>
<h2>References</h2>
<ul>
<li><p><a href="https://docs.microsoft.com/en-us/azure/active-directory/authentication/tutorial-enable-azure-mfa">Enable Azure AD Multi-Factor Authentication - Microsoft Entra Microsoft Docs</a></p>
</li>
<li><p><a href="https://docs.microsoft.com/en-us/azure/active-directory/authentication/tutorial-enable-sspr">Enable Azure Active Directory self-service password reset - Microsoft Entra Microsoft Docs</a></p>
</li>
<li><p><a href="https://learn.hashicorp.com/tutorials/terraform/github-actions">Automate Terraform with GitHub Actions Terraform - HashiCorp Learn</a></p>
</li>
<li><p>Future improvements: Terraform for_each loop through a list of users defined in a CSV as outlined by <a href="https://learn.hashicorp.com/tutorials/terraform/azure-ad?in=terraform/azure">HashiCorp</a> to improve your identity governance strategy as your user base grows increasing in complexity of management on a per-user basis.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Silence "Refreshing state…" & Highlight Changes in GitHub Actions Terraform Plan Output]]></title><description><![CDATA[This article was originally published in November 2022 on my GitHub io blog here



Purpose
After working with GitHub Actions as my Terraform CI pipeline over the past year, I started looking for pote]]></description><link>https://blog.jennasrunbooks.com/silence-refreshing-state-highlight-changes-in-github-actions-terraform-plan-output</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/silence-refreshing-state-highlight-changes-in-github-actions-terraform-plan-output</guid><category><![CDATA[github-actions]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[Devops]]></category><category><![CDATA[ci-cd]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Tue, 07 Mar 2023 18:22:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/4XvAZN8_WHo/upload/d813c9acd4986c5d64f4aec500c7c1ed.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em><strong>This article was originally published in November 2022 on my GitHub io blog</strong></em> <a href="https://jksprattler.github.io/jennas-runbooks/DevOps/CI-CD/ghactions-silence-refreshing-diff.html"><em><strong>here</strong></em></a></p>
</blockquote>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678212901239/9f4ba318-0acd-43c5-a1e7-e85b89c3d269.png" alt="" style="display:block;margin:0 auto" />

<h2>Purpose</h2>
<p>After working with GitHub Actions as my Terraform CI pipeline over the past year, I started looking for potential methods to clean up the Plan outputs displayed in PR comments to provide a more streamlined PR review. I was interested in finding a way to redact the "Refreshing state…" messages as I find them distracting and unnecessary for reviewing. These messages can also get quite lengthy for larger infrastructures containing many resources managed by Terraform. Essentially what Terraform is doing when generating these messages is ensuring that your state files are in alignment with the existing infrastructure.</p>
<p>I also found that you can incorporate the <code>diff</code> utility into your Actions pull-request script section to provide color highlights in the plan output. This runbook will cover both the lines of code needed for the <code>diff</code> utility to display results correctly and a method for silencing "Refreshing state…" messages.</p>
<p>I'll only be discussing the GH Actions jobs for Terraform plan, show, reformatting the plan, creating the plan environment variable and incorporating this into the script section of the pull-request plan output.</p>
<h2>Code Samples</h2>
<ul>
<li><p>Example code snippets will be taken from my workflow on Github <a href="https://github.com/jksprattler/azure-security/blob/main/.github/workflows/azuretfdeploy.yml">here</a></p>
</li>
<li><p>The PR I used for testing the config can be reviewed <a href="https://github.com/jksprattler/azure-security/pull/8">here</a></p>
</li>
</ul>
<h2><strong>GitHub Actions Config</strong></h2>
<ul>
<li><p>In the Terraform Plan job ensure that the <code>-no-color</code> flag is set as without it the output is not rendered correctly by the JavaScript and you'll see garbled text/characters. Apply the <code>-out</code> flag which saves the plan output to a local file and assign it a name:</p>
<pre><code class="language-yaml">      - name: "Terraform Plan"
        id: plan
        run: terraform plan -detailed-exitcode -no-color -out=plan -input=false
        continue-on-error: true
</code></pre>
</li>
<li><p>Create a job for the Terraform Show output. This is going to read the local file of the plan saved in the previous step. Running this job is what will redact all of the "Refreshing state…" messages which get generated by the original terraform plan. Set an if condition to only run the show job when the plan has succeeded or provided exit codes 0 (no changes) or 2 (changes present) and write the contents to a text file:</p>
<pre><code class="language-yaml">      - name: Terraform Show 
        id: show 
        if: steps.plan.outcome == 'success' || steps.plan.outputs.exitcode == '0' || steps.plan.outputs.exitcode == '2'
        run: terraform show -no-color plan &gt; plan.txt
        continue-on-error: true
</code></pre>
</li>
<li><p>Create a job to Reformat the plan contents of the text file and write it to a newly formatted text file. This will render the plan output in a way that the <code>diff</code> utility recognizes changes within the file as its read during the pull-request script workflow. The <code>sed</code> command in the job uses a Regex statement to apply spaces in front of any symbols next to resource actions by pushing them to the first column of the output. This is required for the <code>diff</code> utility to correctly render the output into color highlights for changes.</p>
<pre><code class="language-yaml">      - name: Reformat Plan 
        run: |
          cat plan.txt | sed -E 's/^([[:space:]]+)([-+~])/\2\1/g' &gt; format_plan.txt
        continue-on-error: true
</code></pre>
</li>
<li><p>Create a job to assign the new formatted plan output to a Github Environment variable to call the var from within the pull-request script. Note the line containing <code>"${PLAN:0:65536}"</code> is required for very large plan output as the GitHub database sets a limit of 65536 characters on comments. Without setting this limit, if you were to submit a PR over the limit the pipeline would fail. However, with this setting applied a very large plan would be truncated. In a truncated scenario, the reviewer can navigate to the Actions tab of the repo and analyze the full Terraform Plan job contents of the workflow.</p>
<pre><code class="language-yaml">      - name: Put Plan in Env Var
        run: |
          PLAN=$(cat format_plan.txt)
          echo "PLAN&lt;&lt;EOF" &gt;&gt; $GITHUB_ENV
          echo "${PLAN:0:65536}" &gt;&gt; $GITHUB_ENV
          echo "EOF" &gt;&gt; $GITHUB_ENV     
</code></pre>
</li>
<li><p>Update the pull-request script with the <code>diff</code> utility and the new Plan environment variable:</p>
<pre><code class="language-yaml">            &lt;details&gt;&lt;summary&gt;Show Plan&lt;/summary&gt;
      
            \`\`\`\diff\n
            ${{ env.PLAN }}
            \`\`\`
      
            &lt;/details&gt;
</code></pre>
</li>
</ul>
<h2>Conclusion</h2>
<p>With the Terraform jobs described above in place, the CI pipeline for the PR comments will no longer display the "Refreshing state…" messages and color highlights will be generated for all changes (ie lines with -+~ symbols) to plan output. This provides for an overall cleaner PR comment for the reviewer as seen in this example:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678213144974/cb968bb5-e6d9-4d7f-8b5f-aaa5e2989807.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[AWS Hybrid DNS with Terraform]]></title><description><![CDATA[This article was originally published in July 2022 on my GitHub io blog here

Overview
The purpose of this runbook is to demonstrate the implementation of an AWS Hybrid DNS design and architecture bet]]></description><link>https://blog.jennasrunbooks.com/aws-hybrid-dns-with-terraform</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/aws-hybrid-dns-with-terraform</guid><category><![CDATA[AWS]]></category><category><![CDATA[dns]]></category><category><![CDATA[route53]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[vpc]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Mon, 06 Mar 2023 14:02:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ZiQkhI7417A/upload/8ba8c5b9eb112155f91b3e63d71a5109.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em><strong>This article was originally published in July 2022 on my GitHub io blog</strong></em> <a href="https://jksprattler.github.io/jennas-runbooks/AWS/aws-tf-hybrid-dns.html"><em><strong>here</strong></em></a></p>
</blockquote>
<h2>Overview</h2>
<p>The purpose of this runbook is to demonstrate the implementation of an AWS Hybrid DNS design and architecture between an AWS region hosting private-only subnets and an on-prem private corporate data center. This design intends to simulate a hybrid DNS cloud connectivity setup to an on-prem environment using AWS DirectConnect (DX) however, the actual implementation will provide private DNS resolution over an established inter-region AWS VPC Peering connection through various Route 53 services and Linux Bind DNS server components as detailed below.</p>
<h2>Pre-requisites</h2>
<ul>
<li><p>GitHub Account</p>
</li>
<li><p>AWS Account</p>
</li>
<li><p>AWS configuration and credentials <a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html">setup</a></p>
</li>
<li><p>Terraform <a href="https://learn.hashicorp.com/tutorials/terraform/install-cli">installed</a></p>
</li>
</ul>
<h3><strong>Design Artifacts</strong></h3>
<ul>
<li><p>YouTube <a href="https://youtu.be/iQ6OzaN4hyE">demo recording</a></p>
</li>
<li><p>Diagram: <a href="https://github.com/jksprattler/aws-networking/blob/main/aws-terraform-hybrid-dns/diagrams/Simulated-aws-hybrid-dns.png">Simulated AWS Hybrid DNS Network Design Architecture</a></p>
</li>
<li><p>Diagram: <a href="https://github.com/jksprattler/aws-networking/blob/main/aws-terraform-hybrid-dns/diagrams/Actual-aws-hybrid-dns.png">Actual AWS Hybrid DNS Network Design Architecture</a></p>
</li>
<li><p>Clone/Fork the repo containing the Terraform artifacts for the AWS Hybrid DNS design here: <a href="https://github.com/jksprattler/aws-networking.git">jksprattler/aws-networking</a></p>
<ul>
<li>relevant files are under <code>/aws-terraform-hybrid-dns</code></li>
</ul>
</li>
<li><p>Set up integrated DNS resolution for hybrid networks in Amazon Route 53 - <a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/set-up-integrated-dns-resolution-for-hybrid-networks-in-amazon-route-53.html">AWS Prescriptive Guidance</a></p>
</li>
</ul>
<h2>Diagrams</h2>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678075462441/749c999d-dadb-41ba-8a8c-09670a49fb8a.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678075468511/ecb98913-ac28-41b7-a2b8-1d723f93b261.png" alt="" style="display:block;margin:0 auto" />

<h2>Architecture</h2>
<p>The us-east-2 region will be hosting the <code>micros4l-onprem</code> VPC with a prefix of 192.168.10.0/24 also containing 2 private subnets. Two t2.micro EC2 instances will be deployed here in separate subnet/availability zones called <code>micros4l-onpremdnsa/b</code> and configured with Bind and the zone <a href="http://corp.microgreens4life.org"><code>corp.microgreens4life.org</code></a>. Additionally, the zone for <a href="http://aws.microgreens4life.org"><code>aws.microgreens4life.org</code></a> will be configured on these servers with forwarders set to the AWS Inbound endpoint IP addresses to forward requests for the Web subdomain to the AWS Route 53 resolvers. A third basic t2.micro EC2 instance called <code>micros4l-onpremapp</code> will be deployed which we'll use for testing DNS resolution from "on-prem" to AWS (us-east-1).</p>
<p>The us-east-1 region will be hosting the <code>micros4l-aws</code> VPC with a prefix of 10.10.0.0/16 containing 2 private subnets - note the non-overlapping private IP space between the environments as this is a requirement for VPC peering in addition to DirectConnect (DX) connectivity which is being simulated. Two basic t2.micro EC2 instances, <code>micros4l-awsec2b/b</code> will be deployed here for testing DNS resolution into our simulated Corporate on-prem data center. Each instance is deployed in a separate subnet/availability zone. The Route 53 private hosted zone for <a href="http://aws.microgreens4life.org"><code>aws.microgreens4life.org</code></a> with an A record of <a href="http://web.aws.microgreens4life.org"><code>web.aws.microgreens4life.org</code></a> is configured here. Additionally, Route 53 Inbound and Outbound endpoints are hosted here where each endpoint gets associated with both of the us-east-1 private subnets. The Outbound endpoint will have a forwarding rule for the <a href="http://corp.microgreens4life.org"><code>corp.microgreens4life.org</code></a> zone associated with it which targets the <code>micros4l-onpremdnsa/b</code> Bind servers hosted "on-prem" (us-east-2) to resolve outbound requests to the Corporate subdomains.</p>
<p>All instances will be deployed with settings configured to allow Systems Manager connectivity as this is the only way to connect to these private instances in this environment as none of them will be deployed with a public IP address nor is there any internet gateway created - these are completely isolated private environments.</p>
<h2>Procedure</h2>
<ol>
<li><p>Navigate to the <code>/global/iam</code> directory and run terraform plan/apply:</p>
<pre><code class="language-bash">cd aws-terraform-hybrid-dns/global/iam
terraform plan
terraform apply
</code></pre>
<p>Resources deployed in this terraform module:</p>
<ul>
<li><p><code>roles.tf</code> - IAM instance policy, roles and policy attachments which all EC2 instances in this design will utilize</p>
</li>
<li><p><code>s3.tf</code> - S3 bucket for storing the terraform state files. Update your bucket name here as it must be globally unique</p>
</li>
</ul>
</li>
<li><p>Navigate to the <code>/us-east-2</code> directory and run terraform plan/apply:</p>
<pre><code class="language-bash">cd aws-terraform-hybrid-dns/us-east-2
terraform plan
terraform apply
</code></pre>
<p>Resources deployed in this terraform module:</p>
<ul>
<li><p><code>ec2.tf</code> - micros4l-onpremdnsa/b simulating on-prem Linux Bind/DNS servers and micros4l-onpremapp Linux server</p>
</li>
<li><p><code>vpc.tf</code> - VPC with prefix 192.168.10.0/24, 2x private subnets, private route table associated with the 2x subnets, Security Group and rules allowing SSM access and DNS requests, VPC Endpoints for SSM connectivity</p>
</li>
</ul>
</li>
<li><p>Capture the outputs from the <code>/us-east-2</code> module deployment and save them in a temp text file for use as input in the next step. For example:</p>
<pre><code class="language-bash">onprem-private-rt_id = "rtb-0fae5266503453da8"
onpremdnsa_ip = "192.168.10.11"
onpremdnsb_ip = "192.168.10.236"
onpremvpc_id = "vpc-0ab74c12320891aa3"
</code></pre>
</li>
<li><p>Navigate to the <code>/us-east-1</code> directory and run terraform plan/apply:</p>
<pre><code class="language-bash">cd ../us-east-1
terraform plan
terraform apply
</code></pre>
<p>Resources deployed in this terraform module:</p>
<ul>
<li><p><code>ec2.tf</code> - micros4l-awsec2a/b AWS instances</p>
</li>
<li><p><code>route53.tf</code> - <a href="http://aws.microgreens4life.org">aws.microgreens4life.org</a> Route 53 hosted private zone, <a href="http://web.aws.microgreens4life.org">web.aws.microgreens4life.org</a> A record, Route 53 Inbound endpoint, Route 53 Outbound endpoint for the <a href="http://corp.microgreens4life.org">corp.microgreens4life.org</a> domain with a Forwarding rule pointing to the Corp on-prem environment (us-east-2)</p>
</li>
<li><p><code>vpc.tf</code> - VPC with prefix 10.10.0.0/16, 2x private subnets, private route table associated with the 2x subnets, VPC Peering connectivity between the AWS us-east-1 region to the "on-prem" us-east-2 region, Security Group and rules allowing SSM access and DNS requests, VPC Endpoints for SSM connectivity</p>
</li>
</ul>
</li>
<li><p>Capture the outputs from the <code>/us-east-1</code> module deployment and save them in a temp text file for use as input in the next step. Note you'll only need the "ip" address output from each of the 2 endpoints. For example:</p>
<pre><code class="language-bash">aws_route53_resolver_inbound_endpoint_ips = toset([
  {
 "ip" = "10.10.0.90"     &lt;---- INBOUND_ENDPOINT_IP1
 "ip_id" = "rni-2bc122c23384d09af"
 "subnet_id" = "subnet-0e6a97614d0833b47"
  },
  {
 "ip" = "10.10.10.221"     &lt;---- INBOUND_ENDPOINT_IP2
 "ip_id" = "rni-75c2ecfc30094b3a9"
 "subnet_id" = "subnet-0dce015d7ba12e0de"
  },
])
</code></pre>
</li>
<li><p>In the <a href="https://github.com/jksprattler/aws-networking/blob/main/aws-terraform-hybrid-dns/us-east-2/awszone.forward">awszone.forward</a> file, replace the <code>INBOUND_ENDPOINT_IP1</code> and <code>INBOUND_ENDPOINT_IP2</code> values of the forwarders with the Endpoint IP addresses from the outputs of the previous step.</p>
</li>
<li><p>From the AWS console, navigate to the EC2 instances in the us-east-2 region, select <code>micros4l-onpremdnsa</code> and initiate a connection to it via Session Manager. Enter <code>sudo -i</code> and with your editor of choice, vi or nano into the <code>/etc/named.conf</code> file. Scroll to the end of the file and paste the contents of your updated <a href="https://github.com/jksprattler/aws-networking/blob/main/aws-terraform-hybrid-dns/us-east-2/awszone.forward">awszone.forward</a> file, save and exit. Run the following command to restart the bind service:</p>
<pre><code class="language-bash">systemctl restart named 
systemctl status named
</code></pre>
<p>Using dig or nslookup, test that your local DNS server is resolving the AWS Route 53 private zone/domain for <code>aws.microgreens4life.org</code> now that you've applied the Route 53 endpoint IP addresses into the Bind server's named.conf file. You should see it resolve to the 10.10.x.x private IP space of the us-east-1 AWS VPC where the Route 53 inbound endpoints are hosted. For example:</p>
<pre><code class="language-bash">sh-4.2$ dig web.aws.microgreens4life.org @127.0.0.1 +short
10.10.0.172
10.10.10.31
</code></pre>
<p>Repeat above steps for <code>micros4l-onpremdnsb</code> From the AWS console, navigate to the Route 53 service in the us-east-1 region and validate that the A record hosted in your private Route 53 zone is using the same IP addresses that your DNS server in us-east-2 just resolved to.</p>
</li>
<li><p>Navigate to the EC2 instances in us-east-2 and select <code>micros4l-onpremapp</code> and initiate a connection to it via Session Manager. Enter <code>sudo -i</code> and with your editor of choice, vi or nano into the <code>/etc/sysconfig/network-scripts/ifcfg-eth0</code> file. Scroll to the end of the file and paste the following contents replacing the <code>THE_PRIVATE_IP_OF_ONPREM_DNS_A/B</code> values with the actual private IP addresses of the on-prem DNS servers which were given in the outputs of your terraform apply for the us-east-2 implementation in step 3.):</p>
<pre><code class="language-bash">DNS1=THE_PRIVATE_IP_OF_ONPREM_DNS_A
DNS2=THE_PRIVATE_IP_OF_ONPREM_DNS_B
</code></pre>
<p>Restart the network services: <code>systemctl restart network</code> Run a test ping/dig from the <code>micros4l-onpremapp</code> instance to the AWS route 53 hosted subdomain:</p>
<pre><code class="language-bash">ping web.aws.microgreens4life.org
dig web.aws.microgreens4life.org +short
</code></pre>
</li>
<li><p>Navigate back to the ec2 instances in us-east-1 and initiate a systems manager session on micros4l-awsec2a/b and test DNS resolution of the on-prem hosted subdomain:</p>
<pre><code class="language-bash">ping app.corp.microgreens4life.org
dig app.corp.microgreens4life.org +short
</code></pre>
</li>
<li><p>Cleanup! Run a terraform destroy in each region/module starting with <code>us-east-1</code> - Note I configured ignore lifecycle rules on the accepter_route_table_id and accepter_vpc_id prompts so just hit Enter here to bypass these. You'll need to input the onpremdnsa/b_ip private IP's as I couldn't get a lifecycle rule to work here:</p>
<pre><code class="language-bash">terraform destroy
var.accepter_route_table_id
  Route table id of the accepter that you want to peer with it
  Enter a value: &lt;Enter&gt;
var.accepter_vpc_id
  VPC id that you want to peer with it
  Enter a value: &lt;Enter&gt;
var.onpremdnsa_priv_ip
  Private IP Address of micros4l-onpremdnsa
  Enter a value: 192.168.10.53 &lt;-----onpremdnsa_ip
var.onpremdnsb_priv_ip
  Private IP Address of micros4l-onpremdnsb
  Enter a value: 192.168.10.243 &lt;-----onpremdnsb_ip
</code></pre>
<p>Do the same for <code>us-east-2</code> - No prompts for input on this one, simply just <code>terraform destroy</code> it. I've left the <code>/global/iam</code> resources in tact since it's just an IAM role/policy and S3 bucket storing my terraform state files.</p>
</li>
</ol>
<h2>Summary</h2>
<p>Upon completion of the above procedure, you should now have 2 separate private environments with fully integrated DNS resolution between them. The private AWS VPC instances in us-east-1 are successfully resolving the Corporate subdomain hosted in the private "on-prem" VPC in us-east-2 via the outbound endpoint and forwarding rule for the Corporate domain which gets routed via the VPC Peering connection. Conversely, the private "on-prem" instances in us-east-2 are successfully resolving the web subdomain hosted in the private AWS VPC us-east-1 via the inbound endpoint resolver.</p>
<p><strong>Note</strong> the original idea for this design came from Cloud Trainer, Adrian Cantrill. You can find the CFT stack, procedure steps, and videos for his lab <a href="https://github.com/acantril/learn-cantrill-io-labs/tree/master/aws-hybrid-dns">here</a></p>
<h4><strong>Differences between deployments where I:</strong></h4>
<ul>
<li><p>Coded <strong>all</strong> infrastructure steps in <em>Terraform</em> including vpc peering, vpc peering inter-region routes, route 53 inbound and outbound endpoints and forward rules, etc. instead of CloudFormation (<code>HybridDNS.yaml</code>) used for initial/base infrastructure.</p>
</li>
<li><p>Coded <a href="http://outputs.tf">outputs.tf</a> files to provide values for the input variable strings for the deployments to the separate regions/modules to prevent needing to hunt down id's and ip addresses from within the AWS console (ie vpc peering id, route table id for peering connection, onpremdnsa/b private ip addresses for the aws zone file)</p>
</li>
<li><p>Deployed the simulation of the on-prem / DX connected environment in a completely separate region (us-east-2) instead of all in the same us-east-1 region in an attempt to increase complexity, validate inter-region vpc peering works with DNS resolution against private Route 53 endpoints and just for overall better visualization of connectivity between 2 isolated environments/regions</p>
</li>
<li><p>Chose microgreens4life for my Domain/zone instead of animals4life - nothing against animals, I just really love microgreens. I also replaced all resource names in my code with my variation of m4l/micros4l/microgreens4life which allowed me the opportunity to deeply review the code line by line so I didn't just copy/paste pieces of Adrian's CFT stack code (ie a4l/animals4l/animals4life).</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Jekyll Site Hosted on AWS S3 Using GitHub Actions]]></title><description><![CDATA[This article was originally published in July 2021 on my GitHub io blog here

Overview
The purpose of this runbook is to define the steps needed to deploy a secure static website hosted on an AWS S3 B]]></description><link>https://blog.jennasrunbooks.com/jekyll-site-hosted-on-aws-s3-using-github-actions</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/jekyll-site-hosted-on-aws-s3-using-github-actions</guid><category><![CDATA[Devops]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Amazon S3]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Mon, 06 Mar 2023 03:53:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/MOO6k3RaiwE/upload/80998b103a77696ea5014baf5017152d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em><strong>This article was originally published in July 2021 on my GitHub io blog</strong></em> <a href="https://jksprattler.github.io/jennas-runbooks/DevOps/CI-CD/jekyll-s3-actions.html"><em><strong>here</strong></em></a></p>
</blockquote>
<h2>Overview</h2>
<p>The purpose of this runbook is to define the steps needed to deploy a secure static website hosted on an AWS S3 Bucket served by an AWS CloudFront Distribution (CDN) with automated deployment of updates using a GitHub Actions Workflow.</p>
<p>Our use case for this runbook will be following the build of my personal resume website: <a href="http://jennasprattler.com">jennasprattler.com</a> | <a href="http://www.jennasprattler.com">www.jennasprattler.com</a></p>
<h2>Pre-requisites</h2>
<ul>
<li><p>Static website</p>
<ul>
<li><p>For ideas, there's a curated directory of <a href="https://jekyllthemes.io/">Jekyll Themes</a></p>
</li>
<li><p>I'm using this Jekyll theme for my website: <a href="https://github.com/sproogen/modern-resume-theme">modern-resume-theme</a></p>
</li>
</ul>
</li>
<li><p>GitHub Account and Repo</p>
<ul>
<li>To store your website files and run GitHub Actions Workflows</li>
</ul>
</li>
<li><p>AWS Account</p>
<ul>
<li>To implement Route 53 DNS records, S3 Bucket storage, CloudFront CDN and AWS Certificate Manager for SSL Certificates</li>
</ul>
</li>
</ul>
<h2>High-level Overview - Traffic Flow</h2>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677727388223/48031cc4-bbf1-469c-9a4b-959a5e907abd.jpeg" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p>Mobile and Desktop browsers are supported by the Jekyll website</p>
</li>
<li><p>AWS Route 53 performs DNS resolution for <a href="http://jennasprattler.com">jennasprattler.com</a> and <a href="http://www.jennasprattler.com">www.jennasprattler.com</a></p>
</li>
<li><p>AWS CloudFront CDN serves up any cached content of your website from one of its Edge Locations closest to you</p>
</li>
<li><p>AWS Certificate Manager hosts the SSL Certificate for your website which gets assigned to your CloudFront CDN encrypting all user traffic</p>
</li>
<li><p>AWS S3 Bucket hosts all of your website files whereas the CloudFront CDN retrieves anything that it doesn't have cached on its Edge Location</p>
</li>
</ul>
<h2>High-level Overview - CI/CD Flow</h2>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677727397148/c79d5388-1746-4421-924f-0f9913c2cfba.jpeg" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p>Developer writes the code for the Jekyll Website</p>
</li>
<li><p>GitHub hosts the repo for the Jekyll website files</p>
</li>
<li><p>GitHub Actions kickoff the CI/CD Workflow whenever a push is made to the main branch</p>
</li>
<li><p>CI/CD Workflow uploads the output files from the Jekyll <code>_site</code> directory to the AWS S3 bucket</p>
</li>
<li><p>CloudFront invalidation is run to clear out any cached content and immediately serve the updated S3 website content</p>
</li>
</ul>
<h2>Procedure</h2>
<p><strong>Create Route 53 Domain</strong></p>
<ul>
<li>I'll be creating my domain using Route 53 however, you can use another domain provider if you like just note that you'll need to follow a slightly different procedure than what I've defined in this runbook.</li>
</ul>
<ol>
<li>Navigate to the AWS Route 53 service and check the availability of your domain name - if available purchase it. At the time of this writing, it cost me $12 per year for my new .com domain.</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678073746975/eb853ec1-5c49-4148-a3d9-4765f6738dce.png" alt="" style="display:block;margin:0 auto" />

<p>Your new domain should now show a status of "Domain registration in progress." It will take approximately 30 minutes for your new domain to be registered.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678073789482/707d1642-7310-4571-a427-3f6762e97cf6.png" alt="" style="display:block;margin:0 auto" />

<p>Proceed with the next section to create the S3 bucket. We'll create DNS records for our new site in a later section.</p>
<h4><strong>Create S3 Bucket</strong></h4>
<ol>
<li><p>Copy the Cloudformation stack YAML code below and replace all FIXME values per your environment.</p>
<ul>
<li><p><code>BucketName</code> must match your domain name exactly</p>
</li>
<li><p><code>PublicAccessBlockConfiguration</code> properties are all set to false to allow public access to your website</p>
</li>
<li><p><code>BucketPolicy</code> has <code>s3:GetObject</code> action set to allow anyone can read the object data and view the website</p>
</li>
<li><p><code>WebsiteConfiguration</code> enables the static website capability in S3</p>
</li>
<li><p><code>WWWBucket</code> creates an empty bucket only used to redirect <a href="http://www.FIXME.com">www.FIXME.com</a> traffic to your <a href="http://FIXME.com">FIXME.com</a> bucket; only needed if you decide to not use a CloudFront CDN and just host unencrypted content from S3 only (which is what I did initially)</p>
</li>
</ul>
<pre><code class="language-yaml">---
AWSTemplateFormatVersion: '2010-09-09'


Description: Simple S3 Bucket to host static public website.


Resources:


  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: FIXME.com
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false
      Tags:
        -
          Key: Description
          Value: FIXME
        - Key: Project
          Value: FIXME.com
      VersioningConfiguration:
        Status: Enabled
      WebsiteConfiguration:
        ErrorDocument: 404.html
        IndexDocument: index.html
          
  BucketPolicyDataSync:  
    Type: 'AWS::S3::BucketPolicy'  
    Properties:  
      Bucket:  !Ref S3Bucket
      PolicyDocument:
        Statement:  
        -  
          Sid: "AllowAccesToIAMRole"  
          Action:  
            - "s3:GetObject"
            
          Effect: "Allow"  
          Resource:  
            Fn::Join:  
              - ""  
              -  
                - "arn:aws:s3:::"  
                -  
                  Ref: "S3Bucket"  
                - "/*"  
          Principal: "*"            

  WWWBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: www.FIXME.com 
      AccessControl: BucketOwnerFullControl
      WebsiteConfiguration:
        RedirectAllRequestsTo:
          HostName: FIXME.com

Outputs:


  S3BucketName:
    Value: !Ref S3Bucket
    Description: S3 Bucket for object storage


  S3BucketARN:
    Value: !GetAtt S3Bucket.Arn
    Description: S3 bucket ARN
</code></pre>
<ol>
<li><p>Navigate to the region closest to you and go to the CloudFormation service.</p>
</li>
<li><p>Upload your updated CFT stack to create your new S3 buckets for hosting your static website files and monitor the event progress.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678074044139/3364d6ac-e3bc-44c8-80a5-9362b049d899.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Once created, your S3 bucket permissions and policy should look similar to this:</p>
</li>
</ol>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678074072163/ba09b72d-7bb7-49b4-9aba-f8ac898d92f8.png" alt="" style="display:block;margin:0 auto" />

<p>Capture your S3 Endpoint URL - you can find this by navigating to your new S3 Bucket &gt; Properties &gt; Static web site hosting &gt; Endpoint. We'll need this for the CloudFront section.</p>
<h4><strong>Create Route 53 Hosted Zone and DNS Records</strong></h4>
<ul>
<li>Proceed once the Domain registration has been completed</li>
</ul>
<ol>
<li><p>Navigate to the Route 53 service where you should see your new domain has moved from Pending requests to Registered domains.</p>
</li>
<li><p>Create a Hosted zone for your new domain by entering your Domain Name and selecting Public Hosted Zone in the Type dropdown.</p>
</li>
<li><p>The NS and SOA records for your new domain will auto-create for you.</p>
</li>
<li><p>Proceed to the next section to create your new CloudFront distribution as you'll need to point your new A records to the new CloudFront Domain Name for your website. You can also go through the ACM certificate process to automatically validate your new domain which will automatically create a new CNAME record for you in your hosted zone. Once that's completed, return here to step 5.</p>
</li>
<li><p>In your new public hosted zone, you should now see a CNAME record pointing to your ACM Certificate in addition to the NS and SOA records.</p>
</li>
<li><p>Create 2 new A records, one for your root domain and the other for any sub-domains you want pointed to your root domain (ie <a href="http://www.FIXME.com">www.FIXME.com</a>). Each A record should point to your new CloudFront Domain Name created in the Create CloudFront Distribution section.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678074247256/6cf6ad99-6e71-4f51-8224-59ab19e0ee18.png" alt="" style="display:block;margin:0 auto" /></li>
</ol>
<h4><strong>Create CloudFront Distribution</strong></h4>
<ol>
<li><p>Navigate to the AWS Cloudfront service and select Create distribution &gt; Web &gt; Get started</p>
</li>
<li><p>Under Origin Domain name, paste your S3 Endpoint and remove the prefix, "http://" from the URL. Under Default Cache Behavior Settings &gt; Viewer Protocol Policy select "Redirect HTTP to HTTPS". Leave the remaining Origin settings as default.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678074282816/d18e2981-1750-4e4a-b89d-fcc0cffd62a4.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Under Distribution Settings &gt; Alternate Domain Names (CNAMEs):</p>
<ul>
<li><p>Enter your new domain and any other subdomains (ie <a href="http://www.FIXME.com">www.FIXME.com</a>) you plan to use each on a new line.</p>
</li>
<li><p>Select "Custom SSL Certificate" to secure your website using a stored in Amazon Certificate Manager (ACM) in the US East (N. Virginia) Region. Select the "Request or Import a Certificate with ACM button" which will take you through the process of validating your domain. I went through the <a href="https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html">DNS validation process</a> since my domain was registered using Route 53.</p>
</li>
</ul>
</li>
</ol>
<ul>
<li>NOTE: Your Route 53 domain must have completed registration before you can request to validate it with a new ACM cert. It can take 30+ minutes for your SSL cert to be validated, meanwhile, you can move onto the IAM policy/user creation steps below. Don’t forget to request your ACM cert from us-east-1 which is the only region supported by Cloudfront!</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678074401843/abb038ff-9a88-4989-83b1-3815c1235377.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678074436157/4c198cba-db75-44ba-88aa-8c3dd6e1dbc7.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p>For the Default Root Object, enter: <code>index.html</code></p>
</li>
<li><p>Once your new CDN has been created, copy the 14-character alphanumeric Cloudfront ID and Domain Name (ie <a href="http://FIXME.cloudfront.net">FIXME.cloudfront.net</a>).</p>
</li>
</ul>
<h4><strong>Create IAM Policy and IAM User for Github Actions</strong></h4>
<ol>
<li><p>Navigate to IAM and <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create-console.html#access_policies_create-json-editor">create a new IAM Policy</a> using the JSON editor.</p>
</li>
<li><p>Paste the following policy contents into the JSON editor and update all "FIXME" values for your bucket name, AWS account ID and CloudFront ID:</p>
</li>
</ol>
<pre><code class="language-json">    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Resource": [
                    "arn:aws:s3:::FIXME-bucket-name",
                    "arn:aws:s3:::FIXME-bucket-name/*"
                ],
                "Sid": "VisualEditor1",
                "Effect": "Allow",
                "Action": [
                    "s3:*"
                ]
            },
            {
                "Sid": "VisualEditor2",
                "Effect": "Allow",
                "Action": "cloudfront:*",
                "Resource": "arn:aws:cloudfront::FIXME-aws-account-number:distribution/FIXME-distribution-id"
            }
        ]
    }
</code></pre>
<ol>
<li><p>Create a new IAM User with programmatic access and attach the IAM Policy you just created above.</p>
</li>
<li><p>Copy the AWS access key ID and Secret access key for your user to a safe location such as secrets manager or key vault, as these will be used in the GitHub Action Workflow setup below.</p>
</li>
</ol>
<h4><strong>Create GitHub Action Workflow</strong></h4>
<ol>
<li><p>From within your GitHub repo navigate to: <code>.github/workflows/</code> and create a new file called <code>build-and-deploy.yml</code></p>
</li>
<li><p>Copy and paste the following into your newly created GitHub Action workflow and update the FIXME value for the region that your S3 bucked was deployed in.</p>
</li>
</ol>
<pre><code class="language-yaml">name: CI / CD

# Controls when the action will run. 
on:
  # Triggers the workflow on push for the main branch
  push:
    branches: [ main ]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:
  
env:
  AWS_ACCESS_KEY_ID: $
  AWS_SECRET_ACCESS_KEY: $
  AWS_DEFAULT_REGION: 'FIXME'

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 2.7
        bundler-cache: true
    - uses: actions/cache@v2
      with:
        path: vendor/bundle
        key: $-gems-$
        restore-keys: |
          $-gems-
    - name: Install dependencies
      run: |
        gem install bundler
        gem install jekyll
        bundle config path vendor/bundle
        bundle install --jobs 4 --retry 3        
    - name: "Build Site"
      run: bundle exec jekyll build
      env:
        JEKYLL_PAT: $
    - name: "Deploy to AWS S3"
      run: aws s3 sync ./_site/ s3://$ --acl public-read --delete --cache-control max-age=604800
    - name: "Create AWS Cloudfront Invalidation"
      run: aws cloudfront create-invalidation --distribution-id $ --paths "/*"
</code></pre>
<h4><strong>Create GitHub Action Secrets</strong></h4>
<ol>
<li><p>Navigate to Your Repo &gt; Settings &gt; Secrets &gt; Actions</p>
</li>
<li><p>Configure The GitHub Action Secrets for the following:</p>
</li>
</ol>
<ul>
<li><p><code>AWS_ACCESS_KEY_ID</code> - The AWS access key ID associated with the programmatic IAM User.</p>
</li>
<li><p><code>AWS_SECRET_ACCESS_KEY</code> - The AWS secret key ID associated with the programmatic IAM User.</p>
</li>
<li><p><code>AWS_S3_BUCKET_NAME</code> - Your AWS bucket name hosting your website, for example <a href="http://jennasprattler.com">jennasprattler.com</a> or <a href="http://FIXME.com">FIXME.com</a></p>
</li>
<li><p><code>AWS_CLOUDFRONT_DISTRIBUTION_ID</code> - The 14-character alphanumeric Cloudfront distribution ID fronting your S3 bucket's website.</p>
</li>
<li><p><code>JEKYLL_PAT</code> - Set up a GitHub token that can be used by the workflow to build the Jekyll _site pages.</p>
</li>
</ul>
<h4><strong>Deploy your Jekyll website using GitHub Actions</strong></h4>
<ol>
<li><p>Make a change to your Jekyll site &gt; commit &gt; push the changes to main.</p>
</li>
<li><p>Navigate to Your Repo &gt; Actions and under the CI / CD Workflow select the latest running build to monitor the progress:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678074709427/273b6823-40a0-4f7e-80ee-60bba642ab3c.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Once the Workflow has completed, browse to your website in a new tab or refresh any open tabs to see the updated content.</p>
</li>
</ol>
<p>You now have a functioning, secure, serverless, static website configured for automatic updates upon code commit to your repository!</p>
]]></content:encoded></item><item><title><![CDATA[Network DevOps Transformation]]></title><description><![CDATA[This article was originally published in July 2021 on my GitHub io blog here

Purpose
I gave a presentation on DevOps terminology, roles and strategy to a group of leaders in the networking group of m]]></description><link>https://blog.jennasrunbooks.com/network-devops-transformation</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/network-devops-transformation</guid><category><![CDATA[Devops]]></category><category><![CDATA[devopstransformation]]></category><category><![CDATA[Digital Transformation]]></category><category><![CDATA[networking]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Thu, 02 Mar 2023 03:13:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677726712305/944603f0-9566-421b-a9dc-15b2ebd2b7e0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This article was originally published in July 2021 on my GitHub io blog <a href="https://jksprattler.github.io/jennas-runbooks/DevOps/Concepts/network-devops-transformation.html">here</a></p>
</blockquote>
<h2>Purpose</h2>
<p>I gave a presentation on DevOps terminology, roles and strategy to a group of leaders in the networking group of my employer. The purpose of the presentation was to engage in open dialogue and support facilitating the path to a successful DevOps transformation for our network teams.</p>
<p>The catalyst for my presentation was based on understanding who would be assigned to some very technical-specific DevOps roles. I took some time to put together a slide deck on DevOps topics (linked below) which led to a very fruitful conversation during our call. Based on the dialogue and feedback I received from the presentation, it was apparent that there was a much clearer understanding amongst the group on what a Network DevOps transformation might look like for our specific scenario.</p>
<h2>Background</h2>
<p>Over the past several months a small group of us embarked on a journey into uncharted territory with goals to design a complete CI/CD infrastructure using Azure DevOps by building out the architecture completely from scratch. A key driver for this was automating the upcoming deployments for our global cloud firewall solution which was going to be hosted in all of our landing zones.</p>
<p>We were highly collaborative amongst ourselves and also reached out to engineers on different teams for valuable insight as we progressed. Once we nailed down the design and architecture for our ADO CI/CD infrastructure our next task was to train our network support teams on how to use it all. We held weekly/monthly demos reviewing ADO Git, Wiki, build/release pipelines, etc. and built out lab environments for our different cloud providers to encourage a learning environment within the network teams.</p>
<h2>Presentation</h2>
<p>I've done a mock-up of the "Network DevOps Transformation" presentation as the feedback I received from the group was very positive. My colleagues found the information to be beneficial in their understanding of DevOps and what a transformation from a traditional Operations team into a highly performing DevOps team might look like as an entry point.</p>
<p>I've removed any employer branding and company-specific details from the presentation. What you'll find in the youtube recording and slide deck links below are the core contents of the presentation that I gave originally. This is all publicly available information based on my research and personal DevOps experience over the past couple of years.</p>
<ul>
<li><p>YouTube recording: <a href="https://youtu.be/Rr8Z_OUdNVI">Network DevOps Transformation</a></p>
</li>
<li><p>Slide deck: <a href="https://github.com/jksprattler/devops-concepts/blob/main/network-devops-transformation.pdf">Network DevOps Transformation pdf presentation</a></p>
</li>
</ul>
<h2>Final Thoughts</h2>
<p>I hope you've found this presentation helpful as you move along in your DevOps journey. As I've learned in my research and experience, every DevOps transformation is going to look a little different - there is no one size fits all approach and not everyone will agree on everything. Although we may not always agree, the main goal is to arrive at that self-organizing system of collaboration where all members of the team feel valued and respected.</p>
]]></content:encoded></item><item><title><![CDATA[Terraform Visualization]]></title><description><![CDATA[You can visualize 🔎 the components used to build your terraform plan executions with 𝘁𝗲𝗿𝗿𝗮𝗳𝗼𝗿𝗺 𝘃𝗶𝘀𝘂𝗮𝗹 available at: https://hieven.github.io/terraform-visual/
The config in this gif is]]></description><link>https://blog.jennasrunbooks.com/terraform-visualization</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/terraform-visualization</guid><category><![CDATA[Terraform]]></category><category><![CDATA[Infrastructure as code]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Wed, 22 Feb 2023 01:52:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/xnqVGsbXgV4/upload/9a82231884b176305734858685897fb7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You can visualize 🔎 the components used to build your terraform plan executions with 𝘁𝗲𝗿𝗿𝗮𝗳𝗼𝗿𝗺 𝘃𝗶𝘀𝘂𝗮𝗹 available at: <a href="https://hieven.github.io/terraform-visual/">https://hieven.github.io/terraform-visual/</a></p>
<p>The config in this gif is from a lab I'm working on deploying AWS IAM objects which include local custom modules, meta-arguments, variable types and expressions:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677029863301/e5a445e5-d17f-4ed5-9955-80f6fc4209c2.gif" alt="" style="display:block;margin:0 auto" />

<p>I only recently started looking into terraform visualization tools and have experimented with this one and 𝘁𝗲𝗿𝗿𝗮𝗳𝗼𝗿𝗺 𝗴𝗿𝗮𝗽𝗵 so far. Terraform graph adds a bit of granularity to the visualization as it includes all components such as variables, providers, etc. However, it can be a bit more difficult to follow the code logic at first glance depending on complexity. Here's the png output from my AWS IAM lab using terraform graph:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677023321356/c0afc2cd-5674-4103-a6b5-391023521f85.png" alt="" style="display:block;margin:0 auto" />

<p>If you're on Ubuntu/Debian 🖥 check it out by running the following:</p>
<pre><code class="language-bash">sudo apt-get update
sudo apt install graphviz 
terraform graph -type=plan | dot -Tpng -o graph.png
</code></pre>
<p>Both of these tools 🔧 offer a helpful way to better understand your Terraform infra code particularly if the config is complex, there's little to no documentation on the code logic, or you're newer to Terraform in general. I also like how easy they are to spin up and use plus they're open-source 😎 That said, always be cautious about uploading config files containing private/secure data to a remote website like terraform visual. In those scenarios, it'd be best to run something locally like terraform graph.</p>
<p>Are there other tools you've found useful for visualizing terraform infra?</p>
]]></content:encoded></item><item><title><![CDATA[Boto3 Script to Reset AWS IAM User Passwords]]></title><description><![CDATA[Here's a simple boto3 script 📜 to reset AWS IAM user passwords that I wrote. It's integrated with a password generator tool to match the strict AWS password policy enforced in my environment. It also]]></description><link>https://blog.jennasrunbooks.com/boto3-script-to-reset-aws-iam-user-passwords</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/boto3-script-to-reset-aws-iam-user-passwords</guid><category><![CDATA[boto3]]></category><category><![CDATA[AWS]]></category><category><![CDATA[awssecurity]]></category><category><![CDATA[Python]]></category><category><![CDATA[cloudsecurity]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Wed, 15 Feb 2023 14:03:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/XJXWbfSo2f0/upload/9fd7b889eda1a84579ff56fbb6eeb0a9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here's a simple boto3 script 📜 to reset AWS IAM user passwords that I wrote. It's integrated with a password generator tool to match the strict AWS password policy enforced in my environment. It also includes an optional argument to list all current users allowing you to capture the username you need to reset which can help eliminate some back-and-forth Q&amp;A with the requester.</p>
<p>Pain points I was looking to address with this one:</p>
<p>✅ No more logging into the AWS console UI</p>
<p>✅ No more crafting the "aws iam update-login-profile …" command</p>
<p>✅ No more manually generating passwords to match the strict password policy enforced</p>
<p>Link to Script: <a href="https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_user_password_reset.py">https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_user_password_reset.py</a></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679937835485/a96f3a05-56a7-4108-abb7-fb198f230b22.png" alt="" style="display:block;margin:0 auto" />

<p><img src="align=%22center%22" alt="" /></p>
]]></content:encoded></item><item><title><![CDATA[Resolve Git local changes to the following files would be overwritten]]></title><description><![CDATA[I ran into a new conflict using git today that took a bit of wrangling 🤠
"𝗬𝗼𝘂𝗿 𝗹𝗼𝗰𝗮𝗹 𝗰𝗵𝗮𝗻𝗴𝗲𝘀 𝘁𝗼 𝘁𝗵𝗲 𝗳𝗼𝗹𝗹𝗼𝘄𝗶𝗻𝗴 𝗳𝗶𝗹𝗲𝘀 𝘄𝗼𝘂𝗹𝗱 𝗯𝗲 𝗼𝘃𝗲𝗿𝘄𝗿𝗶𝘁𝘁𝗲𝗻 𝗯𝘆 𝗺𝗲]]></description><link>https://blog.jennasrunbooks.com/resolve-git-local-changes-to-the-following-files-would-be-overwritten</link><guid isPermaLink="true">https://blog.jennasrunbooks.com/resolve-git-local-changes-to-the-following-files-would-be-overwritten</guid><category><![CDATA[Git]]></category><category><![CDATA[Devops]]></category><category><![CDATA[coding]]></category><dc:creator><![CDATA[Jenna's Runbooks]]></dc:creator><pubDate>Fri, 10 Feb 2023 18:20:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/842ofHC6MaI/upload/4080c5a9d8a1afc0c683b1a0476af5ce.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I ran into a new conflict using git today that took a bit of wrangling 🤠</p>
<p>"𝗬𝗼𝘂𝗿 𝗹𝗼𝗰𝗮𝗹 𝗰𝗵𝗮𝗻𝗴𝗲𝘀 𝘁𝗼 𝘁𝗵𝗲 𝗳𝗼𝗹𝗹𝗼𝘄𝗶𝗻𝗴 𝗳𝗶𝗹𝗲𝘀 𝘄𝗼𝘂𝗹𝗱 𝗯𝗲 𝗼𝘃𝗲𝗿𝘄𝗿𝗶𝘁𝘁𝗲𝗻 𝗯𝘆 𝗺𝗲𝗿𝗴𝗲"</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676053010754/be822678-881d-4e0a-9482-d6f29d2e8620.png" alt="" style="display:block;margin:0 auto" />

<p>I didn't have any changes to commit locally, particularly not on the specified file, nor was anything stashed in my local branch. Despite this, Git continued to prevent me from pulling my latest commits to my master branch so that I could perform a rebase.</p>
<p>I tried the following to rectify this:</p>
<pre><code class="language-bash">git pull
git pull --force
git pull origin --force
git stash pop
git stash
git stash drop
git merge --ff-only origin/master
git pull origin master
git reset HEAD
</code></pre>
<p>But none of these ⬆️ worked.</p>
<p>The solution for me was to:</p>
<pre><code class="language-bash">git checkout path/to/file/to/revert
git reset HEAD path/to/file/to/revert
git pull
</code></pre>
<p>I was then able to switch back to my local branch, rebase the master branch, fix some conflicts and move on with my day 😎</p>
<p>Have you run into this particular scenario before? Any other suggested commands that worked for you?</p>
]]></content:encoded></item></channel></rss>