From 17e2ea60ad0879ac04bd04f6adb937c9cce72db7 Mon Sep 17 00:00:00 2001 From: Ryan Lafferty <1731734+lafferrs@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:31:27 -0400 Subject: [PATCH 1/8] initial commit --- .gitignore | 35 +++++ AMIVersionTracker.png | Bin 0 -> 111333 bytes CHANGELOG.md | 21 +++ CODEOWNERS | 1 + CODE_OF_CONDUCT.md | 35 +++++ CONTRIBUTING.md | 21 +++ INSTALL.md | 3 + LICENSE | 201 ++++++++++++++++++++++++++ NOTICE | 16 ++ README.md | 74 +++++++++- example/_providers.tf | 21 +++ example/_variables.tf | 1 + example/main.tf | 0 example/terraform-wrapper.sh | 54 +++++++ example/tracked_images/containers.yml | 28 ++++ example/tracked_images/rhel86.yml | 13 ++ lambdas/ami_tracker_lookup/main.py | 147 +++++++++++++++++++ lambdas/ami_tracker_queuer/main.py | 68 +++++++++ terraform/_data.tf | 1 + terraform/_variables.tf | 49 +++++++ terraform/alarms.tf | 57 ++++++++ terraform/config/dev/backend.tfvars | 6 + terraform/config/dev/variables.tfvars | 24 +++ terraform/cwevents.tf | 9 ++ terraform/dynamodb.tf | 56 +++++++ terraform/iam.tf | 154 ++++++++++++++++++++ terraform/lambda.tf | 77 ++++++++++ terraform/scripts/sync_config.py | 162 +++++++++++++++++++++ terraform/sns.tf | 46 ++++++ terraform/sqs.tf | 55 +++++++ 30 files changed, 1434 insertions(+), 1 deletion(-) create mode 100644 AMIVersionTracker.png create mode 100644 CHANGELOG.md create mode 100644 CODEOWNERS create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 INSTALL.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 example/_providers.tf create mode 100644 example/_variables.tf create mode 100644 example/main.tf create mode 100755 example/terraform-wrapper.sh create mode 100644 example/tracked_images/containers.yml create mode 100644 example/tracked_images/rhel86.yml create mode 100644 lambdas/ami_tracker_lookup/main.py create mode 100644 lambdas/ami_tracker_queuer/main.py create mode 100644 terraform/_data.tf create mode 100644 terraform/_variables.tf create mode 100644 terraform/alarms.tf create mode 100644 terraform/config/dev/backend.tfvars create mode 100644 terraform/config/dev/variables.tfvars create mode 100644 terraform/cwevents.tf create mode 100644 terraform/dynamodb.tf create mode 100644 terraform/iam.tf create mode 100644 terraform/lambda.tf create mode 100755 terraform/scripts/sync_config.py create mode 100644 terraform/sns.tf create mode 100644 terraform/sqs.tf diff --git a/.gitignore b/.gitignore index d8fe4fa..d6e9d53 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,36 @@ /.project +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* +*.zip +*.terraform* +tfplan +python +layer_package +tf-plan.json \ No newline at end of file diff --git a/AMIVersionTracker.png b/AMIVersionTracker.png new file mode 100644 index 0000000000000000000000000000000000000000..44c69e1eb474d01f66889905eca619f010561d3d GIT binary patch literal 111333 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

T&uF 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 literal 0 HcmV?d00001 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b13f447 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to this project will be documented in this file. +We follow the [Semantic Versioning 2.0.0](http://semver.org/) format. + + +## 0.0.0 - 2023-10-05 + +Initial Release + +### Added +- Inital set of functionality (see [README.md]) + +### Deprecated +- Nothing. + +### Removed +- Nothing. + +### Fixed +- Nothing. \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..7b46537 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @lafferrs \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a8336c8 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,35 @@ +### CODE OF CONDUCT +Evernorth Strategic Development, Inc. (“Evernorth”) welcomes contributions to our hosted open source projects. We have adopted this Code of Conduct (the “Code”) to facilitate your participation and to ensure our open source community remains a safe, harassment-free, and collaborative space. + +# Scope +This Code applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +# Our Standards +We strive to make participation in our open source projects open and enjoyable for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex, gender identity, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual orientation. + +Examples of behavior that contributes to a positive environment for our community include: +* Demonstrating empathy and kindness toward other people. +* Being respectful of differing opinions, viewpoints, and experiences. +* Giving and gracefully accepting constructive feedback. +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience. +* Focusing on what is best not just for us as individuals, but for the overall community. + +Examples of unacceptable behavior include: +* The use of sexualized language or imagery, and sexual attention or advances of any kind. +* Trolling, insulting or derogatory comments, and personal or political attacks. +* Public or private harassment. +* Publishing others’ private information, such as a physical or email address, without their explicit permission. +* Other conduct which could reasonably be considered inappropriate in a professional setting. + +# Enforcement +The Evernorth Open Source Leadership Group (the “Leadership Group”) is responsible for clarifying and enforcing this Code and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. +The Leadership Group has the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code, and will communicate reasons for moderation decisions when appropriate. + +# Reporting +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the Leadership Group at [opensource@evernorth.io](mailto:opensource@evernorth.io). All complaints will be reviewed and investigated promptly and fairly, while respecting the privacy and security of the reporter. We will determine if and what response is appropriate, and every matter may not result in a direct response from us. +Consequences for inappropriate behavior may include: (1) a warning with consequences for continued behavior; (2) restricted or suspended access to certain projects or other community members for a specific period of time; (3) temporary suspensions from participation in the community; and (4) permanent bans from participating in the community. Factors that may be considered when imposing various consequences may include, but are not limited to, the seriousness or nature of the harm caused, repeated violations of this Code, and the impact of the activity on our broader open source community. + +# Attribution +This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 2.0, available at [contributor-covenant.org/version/2/0/](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8ae82c9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Contributing + +Thanks for your interest in contributing to this project! + +We welcome any kind of contribution including: + +- Documentation +- Examples +- New features and feature requests +- Bug fixes + +Please open Pull Requests for small changes. + +For larger changes please submit and issue with additional details. + +This issue should outline the following: + +- The use case that your changes are applicable to. +- Steps to reproduce the issue(s) if applicable. +- Detailed description of what your changes would entail. +- Alternative solutions or approaches if applicable. diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..a7aa523 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,3 @@ +# Installation instructions + +Detailed instructions on how to install, configure, and get the project running. If the instructions are minimal, they can reside in the Readme Installation section. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7ee9c89 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright © 2022 Evernorth Strategic Development, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..18fabd6 --- /dev/null +++ b/NOTICE @@ -0,0 +1,16 @@ +[ AMI Tracker ] + +Copyright (c) 2023 Evernorth Strategic Development, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/README.md b/README.md index 4cc664b..76986bb 100644 --- a/README.md +++ b/README.md @@ -1 +1,73 @@ -# aws-ami-tracker \ No newline at end of file +# AMI Version Tracker + +## Purpose + +Once deployed to your AWS Account, this terraform module and associated yaml configuration can be used to track and alert on new versions of AWS and third-party AMIs. This allows for eventing on AMI releases, so you can run a build / configuration processes or act to upgrade your development environment when new upstream AMIs are released. + +For example, this project can serve to fill the gap in notifications of new EKS optimized AMIs as requested here: https://github.com/aws/containers-roadmap/issues/734 + +**Technology stack**: Python, Terraform, AWS Resources: Lambda, Dynamodb, SQS, SNS, and CloudWatch. See architecture diagram below for more information. + +## Prerequisites + +An [Amazon Web Services Account](https://aws.amazon.com) and some basic knowledge of how [Terraform Modules](https://developer.hashicorp.com/terraform/language/modules) work are required to get started with this project. + +## Deploying to your AWS Account + +Please see the [INSTALL](INSTALL.md) document for more information. + +## Configuring a tracker + +To track upstream AMI versions, simply add a yaml file to the `tracked_images` folder. There are two configuration types available, Parameter and Filter. + +### Parameter + +The current version of many Amazon and third-party AMIs can be found in public Parameter Store parameters. Some examples can be found [here](https://docs.aws.amazon.com/eks/latest/userguide/retrieve-ami-id.html), and a guide to finding public parameters can be found [here](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-finding-public-parameters.html). The configuration for tracking an AMI with a public parameter requires 2 fields, Name and ParameterPath. It should look similar to the below. + +```yaml +images: + - Name: al2-eks-1.27 + ParameterPath: "/aws/service/eks/optimized-ami/1.27/amazon-linux-2/recommended/image_id" + - Name: al2-eks-1.27-arm + ParameterPath: "/aws/service/eks/optimized-ami/1.27/amazon-linux-2-arm64/recommended/image_id" +``` + +### Filter + +A filter is required to obtain the version for any AMI that does not have a public Parameter Store parameter. Required fields for this type of configuration include Name and Filter. The Filter field is configured as a list of maps and this field is fed to an ec2 `describe_images` API call. The latest image is found by performing the filter call, sorting the results by creation date, and selecting the newest entry. Documentation for the EC2 `describe_images` call can be found [here](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/describe_images.html). Sample configuration can be found below. You can test your filters by calling `ec2 describe_images` in the AWS CLI. Example: `aws ec2 describe-images --filters "Name=name,Values=RHEL-8.6.0_HVM-*-x86_64-*-Hourly2-GP2" "Name=owner-alias,Values=amazon"`. + +```yaml +--- +images: + - Name: redhat-8.6.0-hvm + Filters: >- + { + 'Name': 'name', + 'Values': ['RHEL-8.6.0_HVM-*-x86_64-*-Hourly2-GP2'] + }, + { + 'Name': 'owner-alias', + 'Values': ['amazon'] + } +``` + +## Solution Architecture + +The solution consists of a DynamoDB table, 2 Lambda functions, an SQS queue, and an SNS Notification. The process is kicked off via EventBridge "cron" type trigger. Filter or Parameter information is stored in the DynamoDB table. Upon trigger, the queuer function sends all lookup data to SQS. The Lookup Lambda reads the SQS Queue and processes each record by querying for the latest version of the specified AMI. If there is a new version (defined by an updated AMI version ID), a notification is sent via SNS. + +![Solution Architecture](./AMIVersionTracker.png) + +## Sample Event + +```json +--- +"v1": { + "Message": "A new version of the amazon-linux-2-eks-1.27 has been released. You are now able to launch new EC2 instances from these AMIs.", + "image": { + "image_name": "amazon-linux-2-eks-1.27", + "image_id": "ami-123456abcdef789" + }, + "region": "us-east-1" +} + +``` \ No newline at end of file diff --git a/example/_providers.tf b/example/_providers.tf new file mode 100644 index 0000000..b686b7e --- /dev/null +++ b/example/_providers.tf @@ -0,0 +1,21 @@ +terraform { + required_version = "~> 1.3.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.0" + } + } + + backend "s3" {} +} + +provider "aws" { + region = var.region + profile = var.profile + default_tags { + tags = { + AppName = var.app_name + } + } +} diff --git a/example/_variables.tf b/example/_variables.tf new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/example/_variables.tf @@ -0,0 +1 @@ + diff --git a/example/main.tf b/example/main.tf new file mode 100644 index 0000000..e69de29 diff --git a/example/terraform-wrapper.sh b/example/terraform-wrapper.sh new file mode 100755 index 0000000..e8bcdfb --- /dev/null +++ b/example/terraform-wrapper.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# export TF_LOG=TRACE +script_dir=${PWD##*/} + +printf "\e[1mTerraform wrapper...\e[0m\n" + +args=("$@") + +if [[ ${#args[@]} -eq 0 ]]; then + terraform + exit 1 +fi + +aws sts get-caller-identity + +infra_root=$(git rev-parse --show-toplevel) + +aws_env=${AWS_ENV:-dev} +tf_flags="-var-file=config/${aws_env}/variables.tfvars" +tf_backend=" --backend-config config/${aws_env}/backend.tfvars" + +echo "backend: ${tf_backend}" +echo "flags: ${tf_flags}" + +run_terraform(){ + terraform $@ +} + +case "$1" in + init) + rm -rf .terraform.lock.hcl + rm -rf .terraform/terraform.tfstate + run_terraform "$@" $tf_backend + ;; + plan) + run_terraform "$@" $tf_flags + ;; + apply) + run_terraform "$@" $tf_flags + ;; + + refresh) + run_terraform "$@" $tf_flags + ;; + + destroy) + run_terraform "$@" $tf_flags + ;; + *) + run_terraform "$@" + ;; +esac diff --git a/example/tracked_images/containers.yml b/example/tracked_images/containers.yml new file mode 100644 index 0000000..bf1a72f --- /dev/null +++ b/example/tracked_images/containers.yml @@ -0,0 +1,28 @@ +--- +images: + - Name: amazon-linux-2-eks-1.24 + ParameterPath: "/aws/service/eks/optimized-ami/1.24/amazon-linux-2/recommended/image_id" + - Name: amazon-linux-2-eks-1.25 + ParameterPath: "/aws/service/eks/optimized-ami/1.25/amazon-linux-2/recommended/image_id" + - Name: amazon-linux-2-eks-1.26 + ParameterPath: "/aws/service/eks/optimized-ami/1.26/amazon-linux-2/recommended/image_id" + - Name: amazon-linux-2-eks-1.27 + ParameterPath: "/aws/service/eks/optimized-ami/1.27/amazon-linux-2/recommended/image_id" + - Name: amazon-linux-2-eks-arm-1.24 + ParameterPath: "/aws/service/eks/optimized-ami/1.24/amazon-linux-2-arm64/recommended/image_id" + - Name: amazon-linux-2-eks-arm-1.25 + ParameterPath: "/aws/service/eks/optimized-ami/1.25/amazon-linux-2-arm64/recommended/image_id" + - Name: amazon-linux-2-eks-arm-1.26 + ParameterPath: "/aws/service/eks/optimized-ami/1.26/amazon-linux-2-arm64/recommended/image_id" + - Name: amazon-linux-2-eks-arm-1.27 + ParameterPath: "/aws/service/eks/optimized-ami/1.27/amazon-linux-2-arm64/recommended/image_id" + - Name: amazon-linux-2-ecs + ParameterPath: "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" + - Name: win2019-eks-1.24 + ParameterPath: "/aws/service/ami-windows-latest/Windows_Server-2019-English-Core-EKS_Optimized-1.24/image_id" + - Name: win2019-eks-1.25 + ParameterPath: "/aws/service/ami-windows-latest/Windows_Server-2019-English-Core-EKS_Optimized-1.25/image_id" + - Name: win2019-eks-1.26 + ParameterPath: "/aws/service/ami-windows-latest/Windows_Server-2019-English-Core-EKS_Optimized-1.26/image_id" + - Name: win2019-eks-1.27 + ParameterPath: "/aws/service/ami-windows-latest/Windows_Server-2019-English-Core-EKS_Optimized-1.27/image_id" diff --git a/example/tracked_images/rhel86.yml b/example/tracked_images/rhel86.yml new file mode 100644 index 0000000..ca96246 --- /dev/null +++ b/example/tracked_images/rhel86.yml @@ -0,0 +1,13 @@ +images: + #aws ec2 describe-images --filters "Name=name,Values=RHEL-8.6.0_HVM-*-x86_64-*-Hourly2-GP2" --query 'sort_by(Images, &CreationDate)' + #ami-0186f9012927dfa39 + - Name: rhel86 + Filters: >- + { + 'Name': 'name', + 'Values': ['RHEL-8.6.0_HVM-*-x86_64-*-Hourly2-GP2'] + }, + { + 'Name': 'owner-alias', + 'Values': ['amazon'] + } \ No newline at end of file diff --git a/lambdas/ami_tracker_lookup/main.py b/lambdas/ami_tracker_lookup/main.py new file mode 100644 index 0000000..f9e1b5e --- /dev/null +++ b/lambdas/ami_tracker_lookup/main.py @@ -0,0 +1,147 @@ +import json +import boto3 +import os +from aws_lambda_powertools import Logger +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger() +region = os.getenv('REGION', 'us-east-1') +table_name = os.getenv('DYNAMODB_TABLE_NAME', 'AmiTracker') +topic_arn = os.environ['SNS_TOPIC_ARN'] + +# return current version of ami image id with filter type +def lookup_filter(source): + try: + # get current version filter type ami + ec2 = boto3.client('ec2') + response = ec2.describe_images( + Filters=source + ) + + # sort images by creationdate release in descending order + response['Images'].sort( + key=lambda x: x['CreationDate'], + reverse=True + ) + + # return current version ami image id + return response['Images'][0]['ImageId'] + + except Exception as e: + logger.error( + f'An error occurred while trying to get latest AMI ID: {e}') + raise e + +# return current version of ami image id with ssm type +def lookup_ssm(source): + try: + # get current version ssm type ami + ssm = boto3.client('ssm') + response = ssm.get_parameter( + Name=source + ) + + # return current version ami image id + return response['Parameter']['Value'] + except Exception as e: + logger.error( + f'An error occurred while trying to get latest AMI ID: {e}') + raise e + + +def ddb_ami_version_update(ami_name, marketplace_ami_current_version): + try: + # write dynamo table/update item + dynamodb = boto3.resource('dynamodb', region_name=region) + table = dynamodb.Table(table_name) + + # update the ami name with newest ami image id + response = table.update_item( + Key={ + 'AmiName': ami_name + }, + UpdateExpression='SET CurrentVersion = :newCurrentVersion', + ExpressionAttributeValues={ + ':newCurrentVersion': marketplace_ami_current_version + }, + ReturnValues='UPDATED_NEW' + ) + return response + except Exception as e: + logger.error( + f'An error occurred while trying to update the AMI version in DynamoDB: {e}') + raise e + + +def sns_new_version_message(ami_name, marketplace_ami_current_version): + if os.environ.get('RESTORE_DB', False): + logger.info('Skipping SNS message for database restoration') + + # data object to send to sns topic + data = { + "v1": { + "Message": f"A new version of the {ami_name} has been released. You are now able to launch new EC2 instances from these AMIs.", + "image": { + "image_name": f"{ami_name}", + "image_id": f"{marketplace_ami_current_version}" + }, + "region": f"{region}" + } + } + + try: + # publish message to sns topic + sns = boto3.client('sns') + response = sns.publish( + TopicArn=topic_arn, + Message=json.dumps(data) + ) + message_id = response['MessageId'] + logger.info( + f'Sns Topic message published successfully. Message id: {message_id}') + except Exception as e: + logger.error(f'Failed to publish message to the SNS topic: {e}') + raise e + + +@logger.inject_lambda_context +def handler(event, context): + try: + body = json.loads(event['Records'][0]['body']) + + # ami name from ddb + ami_name = body['AmiName'] + + ami_filter = body['Filters'] if 'Filters' in body else '' + ssm_parameter = body['ParameterPath'] if 'ParameterPath' in body else '' + + # ami current_version from ddb + ddb_ami_current_version = body['CurrentVersion'] + logger.info( + f'AMI ID in dynamodb for AMI Name: {ami_name} is {ddb_ami_current_version}') + + current_version = '' + if len(ami_filter) > 0: + # current version update from AWS Marketplace Filter Type + current_version = lookup_filter(ami_filter) + else: + # current version update from AWS Marketplace SSM Type + current_version = lookup_ssm(ssm_parameter) + + logger.info( + f'Latest version of AMI Name: {ami_name} is {current_version}') + + # compare the ami image_id version within ddb and newest version from AWS Marketplace + # if not the same version write newest ami image_id version from AWS Marketplace to ddb table and send sns message + if ddb_ami_current_version != current_version: + ddb_ami_version_update(ami_name, current_version) + sns_new_version_message(ami_name, current_version) + else: + # no version change for ami image_id + logger.info( + f'AMI Name: {ami_name} image_id is the same value no new version available') + return None + + except Exception as e: + logger.error(f'Error: {e}') + raise e diff --git a/lambdas/ami_tracker_queuer/main.py b/lambdas/ami_tracker_queuer/main.py new file mode 100644 index 0000000..ddb305d --- /dev/null +++ b/lambdas/ami_tracker_queuer/main.py @@ -0,0 +1,68 @@ +import json +import boto3 +import os + +from aws_lambda_powertools import Logger + +logger = Logger() + +region = os.getenv('REGION', 'us-east-1') +table_name = os.getenv('DYNAMODB_TABLE_NAME', 'AmiTracker') +queue_url = os.environ['QUEUE_URL'] + +def get_ami_trackerdata(): + ami_tracker_list = [] + try: + # ddb table + dynamodb = boto3.resource('dynamodb', region_name=region) + + table = dynamodb.Table(table_name) + # scan items in ddb table + response = table.scan() + items = response['Items'] + # extension of items in dynamodb table per page + while 'LastEvaluatedKey' in response: + response = table.scan( + ExclusiveStartKey=response['LastEvaluatedKey']) + items.extend(response['Items']) + # for every item append to ami_tracker_list + # return ami_tracker_list + for i in range(len(items)): + ami_tracker_list.append(items[i]) + return ami_tracker_list + except Exception as e: + logger.error( + f'An error occured while trying to retrieve data from the DynamoDB table: {e}') + raise e + + +def send_sqs_ami_trackerdata(data): + try: + # sqs queue + sqs = boto3.client('sqs') + # send each item in ami_tracker_list to sqs queue + # return message id of data sent + response = sqs.send_message( + QueueUrl=queue_url, + MessageBody=json.dumps(data) + ) + message_id = response['MessageId'] + logger.info(f'Message id:{message_id} in the queue') + except Exception as e: + logger.error( + f'An error occured while trying to send data to the SQS queue: {e}') + raise e + + +def handler(event, context): + try: + # ami tracker data + ami_tracker = get_ami_trackerdata() + # every item in ami_tracker list send data to sqs + for i in ami_tracker: + print(send_sqs_ami_trackerdata(i)) + + return 'Data retrieval and sending to the SQS queue completed successfully.' + except Exception as e: + logger.error(f'Error: {e}') + raise e diff --git a/terraform/_data.tf b/terraform/_data.tf new file mode 100644 index 0000000..8fc4b38 --- /dev/null +++ b/terraform/_data.tf @@ -0,0 +1 @@ +data "aws_caller_identity" "current" {} diff --git a/terraform/_variables.tf b/terraform/_variables.tf new file mode 100644 index 0000000..e4ed884 --- /dev/null +++ b/terraform/_variables.tf @@ -0,0 +1,49 @@ +variable "profile" { + type = string + default = "saml" +} + +variable "region" { + type = string + default = "us-east-1" +} + +variable "environment" { + description = "Environment of the deployment" + type = string + default = "" +} + +variable "app_name" { + type = string + default = "AmiTracker" +} + +variable "lambda_environment" { + type = map(string) + default = { + LOG_LEVEL = "INFO" #set to ERROR in PROD + POWERTOOLS_SERVICE_NAME = "AmiTracker" + POWERTOOLS_LOGGER_LOG_EVENT = true #remove or set false for PROD + } +} + +variable "ami_tracker_queuer_event_rule" { + type = string + default = "rate(24 hours)" +} + +variable "custom_alarm_sns" { + type = string + default = "" +} + +variable "deploy_alarm_sns" { + type = bool + default = false +} + +variable "deploy_alarms" { + type = bool + default = false +} \ No newline at end of file diff --git a/terraform/alarms.tf b/terraform/alarms.tf new file mode 100644 index 0000000..fc50c33 --- /dev/null +++ b/terraform/alarms.tf @@ -0,0 +1,57 @@ +locals { + alarm_action = length(var.custom_alarm_sns) ? var.custom_alarm_sns : aws_sns_topic.ami_tracker_ami_alarm.arn +} + +resource "aws_cloudwatch_metric_alarm" "ami-tracker-queuer-lambda-failures" { + count = var.deploy_alarms ? 1 : 0 + alarm_name = "ami-tracker-queuer-lambda-failures" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = 1 + threshold = 1 + alarm_description = "${var.environment} | INFO | ${var.app_name} | AMI Tracker Queuer Lambda - Failed" + alarm_actions = [local.local.alarm_action] + treat_missing_data = "notBreaching" + metric_name = "Errors" + namespace = "AWS/Lambda" + period = 300 + statistic = "Maximum" + dimensions = { + FunctionName = aws_lambda_function.ami_tracker_queuer.function_name + } +} + +resource "aws_cloudwatch_metric_alarm" "ami-tracker-lookup-lambda-failures" { + count = var.deploy_alarms ? 1 : 0 + alarm_name = "ami-tracker-lookup-lambda-failures" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = 1 + threshold = 1 + alarm_description = "${var.environment} | INFO | ${var.app_name} | AMI Tracker Lookup Lambda - Failed" + alarm_actions = [local.local.alarm_action] + treat_missing_data = "notBreaching" + metric_name = "Errors" + namespace = "AWS/Lambda" + period = 300 + statistic = "Maximum" + dimensions = { + FunctionName = aws_lambda_function.ami_tracker_lookup.function_name + } +} + +resource "aws_cloudwatch_metric_alarm" "ami-tracker-dlq-alarm" { + count = var.deploy_alarms ? 1 : 0 + alarm_name = "ami-tracker-dlq-alarm" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = 1 + threshold = 1 + alarm_description = "${var.environment} | INFO | ${var.app_name} | ami-tracker-dlq has messages in the queue!" + alarm_actions = [local.local.alarm_action] + treat_missing_data = "notBreaching" + metric_name = "ApproximateNumberOfMessagesVisible" + namespace = "AWS/SQS" + period = 900 + statistic = "Sum" + dimensions = { + QueueName = aws_sqs_queue.ami_tracker_lookup_dlq.name + } +} \ No newline at end of file diff --git a/terraform/config/dev/backend.tfvars b/terraform/config/dev/backend.tfvars new file mode 100644 index 0000000..11fdfe8 --- /dev/null +++ b/terraform/config/dev/backend.tfvars @@ -0,0 +1,6 @@ +bucket = "cigna-tf-state-832255094218" +dynamodb_table = "cigna-tf-lock-832255094218" +encrypt = true +key = "ami-marketplace-tracker/terraform.tfstate" +profile = "saml" +region = "us-east-1" diff --git a/terraform/config/dev/variables.tfvars b/terraform/config/dev/variables.tfvars new file mode 100644 index 0000000..edb2489 --- /dev/null +++ b/terraform/config/dev/variables.tfvars @@ -0,0 +1,24 @@ +environment = "dev" +required_tags = { + CostCenter = "00004706" + AssetOwner = "cloudcoe@cigna.com" + ServiceNowBA = "BA15820" + ServiceNowAS = "AS031482" + SecurityReviewID = "RITM5183852" + AppName = "AmiTracker" + AssetName = "AmiTracker" + BackupOwner = "cloudcoe@cigna.com" + Purpose = "AmiTracker" +} + +data_at_rest_tags = { + ComplianceDataCategory = "none" + DataSubjectArea = "it" + DataClassification = "internal" + LineOfBusiness = "none" + BusinessEntity = "none" + BackupPlan = "Custom" +} + +lambda_environment = { +} diff --git a/terraform/cwevents.tf b/terraform/cwevents.tf new file mode 100644 index 0000000..51eb803 --- /dev/null +++ b/terraform/cwevents.tf @@ -0,0 +1,9 @@ +resource "aws_cloudwatch_event_rule" "ami_tracker_queuer_event_rule" { + name = "ami_tracker_queuer_event_rule" + schedule_expression = var.ami_tracker_queuer_event_rule +} + +resource "aws_cloudwatch_event_target" "ami_tracker_queuer_event_rule_target" { + rule = aws_cloudwatch_event_rule.ami_tracker_queuer_event_rule.name + arn = aws_lambda_function.ami_tracker_queuer.arn +} \ No newline at end of file diff --git a/terraform/dynamodb.tf b/terraform/dynamodb.tf new file mode 100644 index 0000000..76a0b16 --- /dev/null +++ b/terraform/dynamodb.tf @@ -0,0 +1,56 @@ +locals { + ami_tracker_key_alias = "alias/ami-tracker-ddb-key" +} + +resource "aws_kms_key" "ami_tracker_table_key" { + description = "KMS key for the ami tracker table" + enable_key_rotation = true + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + AWS = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + "${data.aws_caller_identity.current.arn}" + ] + } + Action = "kms:*" + Resource = "*" + } + ] + }) +} + +resource "aws_kms_alias" "ami_tracker_table_key_alias" { + name = local.ami_tracker_key_alias + target_key_id = aws_kms_key.ami_tracker_table_key.key_id +} + +resource "aws_dynamodb_table" "ami_tracker" { + name = var.app_name + billing_mode = "PAY_PER_REQUEST" + hash_key = "AmiName" + stream_enabled = false + tags = var.data_at_rest_tags + attribute { + name = "AmiName" + type = "S" + } + + server_side_encryption { + enabled = true + kms_key_arn = aws_kms_key.ami_tracker_table_key.arn + } +} + +resource "null_resource" "syncronize_data" { + provisioner "local-exec" { + command = <<-EOT + python3 -m sync_config.py -r ${var.region} -t ${aws_dynamodb_table.ami_tracker.name} + EOT + working_dir = "${path.module}/scripts" + } +} + diff --git a/terraform/iam.tf b/terraform/iam.tf new file mode 100644 index 0000000..2a9f624 --- /dev/null +++ b/terraform/iam.tf @@ -0,0 +1,154 @@ + +resource "aws_iam_role" "ami_tracker_queuer_role" { + name = "ami-tracker-queuer" + force_detach_policies = true + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy" "ami_tracker_queuer_policy" { + name = "ami-tracker-queuer-policy" + role = aws_iam_role.ami_tracker_queuer_role.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "dynamodb:Scan", + ] + Resource = aws_dynamodb_table.ami_tracker.arn + }, + { + Effect = "Allow" + Action = [ + "sqs:SendMessage", + ] + Resource = aws_sqs_queue.ami_tracker_lookup_queue.arn + }, + { + Action = [ + "kms:ListKeys", + "kms:ListAliases", + "kms:Describe*", + "kms:Decrypt", + "kms:GenerateDataKey" + ] + Effect = "Allow" + Resource = [ + aws_kms_key.ami_tracker_table_key.arn, + aws_kms_key.ami_tracker_sqs_key.arn + ] + } + ] + }) +} + +resource "aws_iam_role" "ami_tracker_lookup_role" { + name = "ami-tracker-lookup" + force_detach_policies = true + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy" "ami_tracker_lookup_policy" { + name = "ami-tracker-lookup-policy" + role = aws_iam_role.ami_tracker_lookup_role.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "dynamodb:UpdateItem", + ] + Resource = aws_dynamodb_table.ami_tracker.arn + }, + { + Effect = "Allow" + Action = [ + "ec2:DescribeImages", + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "sqs:DeleteMessage", + "sqs:ReceiveMessage", + "sqs:GetQueueAttributes" + ] + Resource = aws_sqs_queue.ami_tracker_lookup_queue.arn + }, + { + Effect = "Allow" + Action = [ + "ec2:DescribeImages", + "sns:ListTopics", + "ssm:GetParameter" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "sns:Publish" + ] + Resource = aws_sns_topic.ami_tracker_ami_notification.arn + }, + { + Action = [ + "kms:ListKeys", + "kms:ListAliases", + "kms:Describe*", + "kms:Decrypt", + "kms:GenerateDataKey" + ] + Effect = "Allow" + Resource = [ + aws_kms_key.ami_tracker_table_key.arn, + aws_kms_key.ami_tracker_sns_key.arn, + aws_kms_key.ami_tracker_sqs_key.arn + ] + } + ] + }) +} \ No newline at end of file diff --git a/terraform/lambda.tf b/terraform/lambda.tf new file mode 100644 index 0000000..a73630b --- /dev/null +++ b/terraform/lambda.tf @@ -0,0 +1,77 @@ +locals { + ami_tracker_queuer_package = "ami_tracker_queuer.zip" + ami_tracker_lookup_package = "ami_tracker_lookup.zip" + queuer_env = merge( + { for key, value in var.lambda_environment : key => value }, + { "QUEUE_URL" : aws_sqs_queue.ami_tracker_lookup_queue.url }, + { "DYNAMODB_TABLE_NAME" : aws_dynamodb_table.ami_tracker.name }, + { "REGION" : var.region }) + lookup_env = merge( + { for key, value in var.lambda_environment : key => value }, + { "SNS_TOPIC_ARN" : aws_sns_topic.ami_tracker_ami_notification.arn }, + { "DYNAMODB_TABLE_NAME" : aws_dynamodb_table.ami_tracker.name }, + { "REGION" : var.region }) +} + +data "archive_file" "ami_tracker_queuer_lambda" { + type = "zip" + source_file = "../lambdas/ami_tracker_queuer/main.py" + output_path = local.ami_tracker_queuer_package +} + +data "archive_file" "ami_tracker_lookup_lambda" { + type = "zip" + source_file = "../lambdas/ami_tracker_lookup/main.py" + output_path = local.ami_tracker_lookup_package +} + +resource "aws_lambda_function" "ami_tracker_queuer" { + filename = local.ami_tracker_queuer_package + function_name = "ami-tracker-queuer" + role = aws_iam_role.ami_tracker_queuer_role.arn + handler = "main.handler" + timeout = 180 + memory_size = 150 + source_code_hash = data.archive_file.ami_tracker_queuer_lambda.output_base64sha256 + layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:26"] + runtime = "python3.9" + + dynamic "environment" { + for_each = length(keys(local.queuer_env)) == 0 ? [] : [true] + content { + variables = local.queuer_env + } + } +} + +resource "aws_lambda_permission" "allow_cloudwatch_to_trigger_ami_tracker_queuer" { + statement_id = "AllowExecutionFromCloudWatch" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.ami_tracker_queuer.arn + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.ami_tracker_queuer_event_rule.arn +} + +resource "aws_lambda_event_source_mapping" "ami_tracker_lookup_sqs_trigger" { + event_source_arn = aws_sqs_queue.ami_tracker_lookup_queue.arn + function_name = aws_lambda_function.ami_tracker_lookup.arn + batch_size = 1 +} + +resource "aws_lambda_function" "ami_tracker_lookup" { + filename = local.ami_tracker_lookup_package + function_name = "ami-tracker-lookup" + role = aws_iam_role.ami_tracker_lookup_role.arn + handler = "main.handler" + timeout = 180 + memory_size = 150 + source_code_hash = data.archive_file.ami_tracker_lookup_lambda.output_base64sha256 + layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:26"] + runtime = "python3.9" + dynamic "environment" { + for_each = length(keys(local.lookup_env)) == 0 ? [] : [true] + content { + variables = local.lookup_env + } + } +} \ No newline at end of file diff --git a/terraform/scripts/sync_config.py b/terraform/scripts/sync_config.py new file mode 100755 index 0000000..d49bf58 --- /dev/null +++ b/terraform/scripts/sync_config.py @@ -0,0 +1,162 @@ +import ast +import boto3 +import logging +import os +import yaml +import argparse + +p = argparse.ArgumentParser() +p.add_argument('-r', '--region', dest='region', action='store', default='us-east-1', + help='Region where your Dynamodb table resides', required=True) +p.add_argument('-t', '--table_name', dest='table_name', action='store', default='AmiTracker', + help='Name of the DynamoDB table to store lookup config', required=True) + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +def load_yaml_config(): + tracked_images_yaml = [] + with os.scandir('../../tracked_images') as images_config: + for entry in images_config: + if entry.name.endswith('.yml') and entry.is_file(): + with open(f'{entry.path}') as f: + yaml_config = yaml.safe_load(f) + tracked_images_yaml += yaml_config['images'] + + for img in tracked_images_yaml: + if 'Filters' in img: + filters = img['Filters'] + # HACK: We want to store this data as an array of maps + # to easily pass to the Filters parameter of the ec2 describe_images call + # add a trailing comma for the ast.literal_eval call to ensure dynamo stores this properly + if img['Filters'].count('{') == 1: + filters = filters + ',' + + img['Filters'] = [a for a in ast.literal_eval(filters)] + + return tracked_images_yaml + + +def load_db_config(table): + response = None + lastKey = None + tracked_images_db = [] + while True: + if lastKey is not None: + response = table.scan(ExclusiveStartKey=lastKey) + else: + response = table.scan() + if 'Items' in response: + for item in response['Items']: + tracked_images_db.append(item) + + if 'LastEvaluatedKey' in response: + lastKey = response['LastEvaluatedKey'] + else: + break + + return tracked_images_db + + +def compare_lists(tracked_images_yaml, tracked_images_db): + removed = [] + changed = [] + new = [new_img for new_img in tracked_images_yaml if new_img['Name'] + not in [img['AmiName'] for img in tracked_images_db]] + + for db_image in tracked_images_db: + yaml = [yml_img for yml_img in tracked_images_yaml if yml_img['Name'] + == db_image['AmiName']] + + if len(yaml) == 0: + removed.append(db_image) + else: + image = yaml[0] + if 'Filters' in image and image['Filters'] != db_image['Filters']: + changed.append(image) + if 'ParameterPath' in image and image['ParameterPath'] != db_image['ParameterPath']: + changed.append(image) + + return new, changed, removed + + +def insert_config(table, new_images): + for img in new_images: + if 'Filters' in img: + item = { + 'AmiName': img['Name'], + 'Filters': img['Filters'], + 'CurrentVersion': '' + } + else: + item = { + 'AmiName': img['Name'], + 'ParameterPath': img['ParameterPath'], + 'CurrentVersion': '' + } + + response = table.put_item( + Item=item + ) + + +def update_config(table, updated_images): + for img in updated_images: + + if 'Filters' in img: + update_expression = 'SET Filters = :newValue' + value = img['Filters'] + else: + update_expression = 'SET ParameterPath = :newValue' + value = img['ParameterPath'] + + try: + response = table.update_item( + Key={ + 'AmiName': img['Name'] + }, + UpdateExpression=update_expression, + ExpressionAttributeValues={ + ':newValue': value + }, + ReturnValues='UPDATED_NEW' + ) + return response + except Exception as e: + logger.error( + f'An error occurred while trying to update the AMI version in DynamoDB: {e}') + raise e + + +def delete_config(table, deleted_images): + for img in deleted_images: + try: + print(img) + table.delete_item(Key={'AmiName': img['AmiName']}) + except Exception as e: + logger.error( + f'An error occurred while trying to update the AMI version in DynamoDB: {e}') + raise e + + +def main(**kwargs): + + ddb_resource = boto3.resource('dynamodb', region_name=args.region) + + table = ddb_resource.Table(args.table_name) + + tracked_images_yaml = load_yaml_config() + tracked_images_db = load_db_config(table) + + new, changed, removed = compare_lists( + tracked_images_yaml, tracked_images_db) + + insert_config(table, new) + update_config(table, changed) + delete_config(table, removed) + + +if __name__ == '__main__': + args = p.parse_args() + main(**vars(args)) diff --git a/terraform/sns.tf b/terraform/sns.tf new file mode 100644 index 0000000..77cd24b --- /dev/null +++ b/terraform/sns.tf @@ -0,0 +1,46 @@ +locals { + ami_tracker_sns_key_alias = "alias/ami-tracker-sns-key" +} + +resource "aws_kms_key" "ami_tracker_sns_key" { + description = "KMS key for the sns topic" + enable_key_rotation = true + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + AWS = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + "${data.aws_caller_identity.current.arn}" + ] + } + Action = "kms:*" + Resource = "*" + } + ] + }) +} + +resource "aws_kms_alias" "ami_tracker_sns_key_alias" { + name = local.ami_tracker_sns_key_alias + target_key_id = aws_kms_key.ami_tracker_sns_key.key_id +} + +resource "aws_sns_topic" "ami_tracker_ami_notification" { + name = "ami-tracker-ami-notification" + kms_master_key_id = aws_kms_key.ami_tracker_sns_key.arn +} + +resource "aws_sns_topic" "ami_tracker_ami_alarm" { + count = var.deploy_alarm_sns ? 1 : 0 + name = "ami-tracker-ami-alarm" + kms_master_key_id = aws_kms_key.ami_tracker_sns_key.arn +} + +resource "aws_ssm_parameter" "ami_tracker_sns_param" { + name = "ami-tracker-sns-topic-arn" + type = "String" + value = aws_sns_topic.ami_tracker_ami_notification.arn +} \ No newline at end of file diff --git a/terraform/sqs.tf b/terraform/sqs.tf new file mode 100644 index 0000000..eddb9fc --- /dev/null +++ b/terraform/sqs.tf @@ -0,0 +1,55 @@ +locals { + ami_tracker_sqs_key_alias = "alias/ami-tracker-sqs-key" +} + +resource "aws_sqs_queue" "ami_tracker_lookup_queue" { + name = "ami-tracker-lookup-queue" + delay_seconds = 90 + max_message_size = 2048 + message_retention_seconds = 86400 + receive_wait_time_seconds = 10 + visibility_timeout_seconds = 180 + kms_master_key_id = aws_kms_alias.ami_tracker_sqs_key_alias.target_key_arn + + redrive_policy = jsonencode({ + deadLetterTargetArn = aws_sqs_queue.ami_tracker_lookup_dlq.arn + maxReceiveCount = 10 + }) + +} + +resource "aws_sqs_queue" "ami_tracker_lookup_dlq" { + name = "ami-tracker-lookup-dlq" + delay_seconds = 90 + max_message_size = 2048 + message_retention_seconds = 86400 + receive_wait_time_seconds = 10 + visibility_timeout_seconds = 180 + kms_master_key_id = aws_kms_alias.ami_tracker_sqs_key_alias.target_key_arn +} + +resource "aws_kms_key" "ami_tracker_sqs_key" { + description = "KMS key for the ami tracker table" + enable_key_rotation = true + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + AWS = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + "${data.aws_caller_identity.current.arn}" + ] + } + Action = "kms:*" + Resource = "*" + } + ] + }) +} + +resource "aws_kms_alias" "ami_tracker_sqs_key_alias" { + name = local.ami_tracker_sqs_key_alias + target_key_id = aws_kms_key.ami_tracker_sqs_key.key_id +} \ No newline at end of file From d82144fadbdb79789d96c0b78b8ac7e5a148e166 Mon Sep 17 00:00:00 2001 From: Ryan Lafferty <1731734+lafferrs@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:09:10 -0400 Subject: [PATCH 2/8] app_name var. readme updates. working example --- .gitignore | 3 ++- README.md | 2 +- example/_providers.tf | 4 +--- example/_variables.tf | 14 ++++++++++++++ example/main.tf | 9 +++++++++ terraform/_variables.tf | 8 ++------ terraform/alarms.tf | 8 ++++---- terraform/config/dev/backend.tfvars | 6 ------ terraform/config/dev/variables.tfvars | 24 ------------------------ terraform/dynamodb.tf | 6 +++--- terraform/iam.tf | 8 ++++---- terraform/lambda.tf | 4 ++-- terraform/scripts/sync_config.py | 7 ++++++- terraform/sns.tf | 2 +- 14 files changed, 49 insertions(+), 56 deletions(-) delete mode 100644 terraform/config/dev/backend.tfvars delete mode 100644 terraform/config/dev/variables.tfvars diff --git a/.gitignore b/.gitignore index d6e9d53..bfca271 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ override.tf.json tfplan python layer_package -tf-plan.json \ No newline at end of file +tf-plan.json +**__pycache__ diff --git a/README.md b/README.md index 76986bb..ccf1c20 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Purpose -Once deployed to your AWS Account, this terraform module and associated yaml configuration can be used to track and alert on new versions of AWS and third-party AMIs. This allows for eventing on AMI releases, so you can run a build / configuration processes or act to upgrade your development environment when new upstream AMIs are released. +Once deployed to your AWS Account, the resources and associated yaml configuration in this terraform module can be used to track and alert on new versions of AWS and third-party AMIs. This allows for eventing on AMI releases, so you can run a build / configuration processes or act to upgrade your development environment when new upstream AMIs are released. For example, this project can serve to fill the gap in notifications of new EKS optimized AMIs as requested here: https://github.com/aws/containers-roadmap/issues/734 diff --git a/example/_providers.tf b/example/_providers.tf index b686b7e..766580f 100644 --- a/example/_providers.tf +++ b/example/_providers.tf @@ -1,13 +1,11 @@ terraform { - required_version = "~> 1.3.0" + required_version = "~> 1.5.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } } - - backend "s3" {} } provider "aws" { diff --git a/example/_variables.tf b/example/_variables.tf index 8b13789..2615a8d 100644 --- a/example/_variables.tf +++ b/example/_variables.tf @@ -1 +1,15 @@ +variable "region" { + type = string + default = "us-east-2" +} + +variable "profile" { + type = string + default = "saml" +} + +variable "app_name" { + type = string + default = "AMI-Tracker" +} diff --git a/example/main.tf b/example/main.tf index e69de29..b076031 100644 --- a/example/main.tf +++ b/example/main.tf @@ -0,0 +1,9 @@ +module "ami-tracker" { + source = "../terraform" + + app_name = "AMI-Tracker-Thingy" + environment = "dev" + deploy_alarms = true + deploy_alarm_sns = true + region = var.region +} \ No newline at end of file diff --git a/terraform/_variables.tf b/terraform/_variables.tf index e4ed884..4ed1d2c 100644 --- a/terraform/_variables.tf +++ b/terraform/_variables.tf @@ -1,7 +1,3 @@ -variable "profile" { - type = string - default = "saml" -} variable "region" { type = string @@ -40,10 +36,10 @@ variable "custom_alarm_sns" { variable "deploy_alarm_sns" { type = bool - default = false + default = true } variable "deploy_alarms" { type = bool - default = false + default = true } \ No newline at end of file diff --git a/terraform/alarms.tf b/terraform/alarms.tf index fc50c33..93a4b32 100644 --- a/terraform/alarms.tf +++ b/terraform/alarms.tf @@ -1,5 +1,5 @@ locals { - alarm_action = length(var.custom_alarm_sns) ? var.custom_alarm_sns : aws_sns_topic.ami_tracker_ami_alarm.arn + alarm_action = length(var.custom_alarm_sns) > 0 ? var.custom_alarm_sns : aws_sns_topic.ami_tracker_ami_alarm[0].arn } resource "aws_cloudwatch_metric_alarm" "ami-tracker-queuer-lambda-failures" { @@ -9,7 +9,7 @@ resource "aws_cloudwatch_metric_alarm" "ami-tracker-queuer-lambda-failures" { evaluation_periods = 1 threshold = 1 alarm_description = "${var.environment} | INFO | ${var.app_name} | AMI Tracker Queuer Lambda - Failed" - alarm_actions = [local.local.alarm_action] + alarm_actions = [local.alarm_action] treat_missing_data = "notBreaching" metric_name = "Errors" namespace = "AWS/Lambda" @@ -27,7 +27,7 @@ resource "aws_cloudwatch_metric_alarm" "ami-tracker-lookup-lambda-failures" { evaluation_periods = 1 threshold = 1 alarm_description = "${var.environment} | INFO | ${var.app_name} | AMI Tracker Lookup Lambda - Failed" - alarm_actions = [local.local.alarm_action] + alarm_actions = [local.alarm_action] treat_missing_data = "notBreaching" metric_name = "Errors" namespace = "AWS/Lambda" @@ -45,7 +45,7 @@ resource "aws_cloudwatch_metric_alarm" "ami-tracker-dlq-alarm" { evaluation_periods = 1 threshold = 1 alarm_description = "${var.environment} | INFO | ${var.app_name} | ami-tracker-dlq has messages in the queue!" - alarm_actions = [local.local.alarm_action] + alarm_actions = [local.alarm_action] treat_missing_data = "notBreaching" metric_name = "ApproximateNumberOfMessagesVisible" namespace = "AWS/SQS" diff --git a/terraform/config/dev/backend.tfvars b/terraform/config/dev/backend.tfvars deleted file mode 100644 index 11fdfe8..0000000 --- a/terraform/config/dev/backend.tfvars +++ /dev/null @@ -1,6 +0,0 @@ -bucket = "cigna-tf-state-832255094218" -dynamodb_table = "cigna-tf-lock-832255094218" -encrypt = true -key = "ami-marketplace-tracker/terraform.tfstate" -profile = "saml" -region = "us-east-1" diff --git a/terraform/config/dev/variables.tfvars b/terraform/config/dev/variables.tfvars deleted file mode 100644 index edb2489..0000000 --- a/terraform/config/dev/variables.tfvars +++ /dev/null @@ -1,24 +0,0 @@ -environment = "dev" -required_tags = { - CostCenter = "00004706" - AssetOwner = "cloudcoe@cigna.com" - ServiceNowBA = "BA15820" - ServiceNowAS = "AS031482" - SecurityReviewID = "RITM5183852" - AppName = "AmiTracker" - AssetName = "AmiTracker" - BackupOwner = "cloudcoe@cigna.com" - Purpose = "AmiTracker" -} - -data_at_rest_tags = { - ComplianceDataCategory = "none" - DataSubjectArea = "it" - DataClassification = "internal" - LineOfBusiness = "none" - BusinessEntity = "none" - BackupPlan = "Custom" -} - -lambda_environment = { -} diff --git a/terraform/dynamodb.tf b/terraform/dynamodb.tf index 76a0b16..b952f9d 100644 --- a/terraform/dynamodb.tf +++ b/terraform/dynamodb.tf @@ -1,5 +1,5 @@ locals { - ami_tracker_key_alias = "alias/ami-tracker-ddb-key" + ami_tracker_key_alias = "alias/${var.app_name}-ddb-key" } resource "aws_kms_key" "ami_tracker_table_key" { @@ -33,7 +33,6 @@ resource "aws_dynamodb_table" "ami_tracker" { billing_mode = "PAY_PER_REQUEST" hash_key = "AmiName" stream_enabled = false - tags = var.data_at_rest_tags attribute { name = "AmiName" type = "S" @@ -46,9 +45,10 @@ resource "aws_dynamodb_table" "ami_tracker" { } resource "null_resource" "syncronize_data" { + depends_on = [ aws_dynamodb_table.ami_tracker ] provisioner "local-exec" { command = <<-EOT - python3 -m sync_config.py -r ${var.region} -t ${aws_dynamodb_table.ami_tracker.name} + python3 ./sync_config.py -r ${var.region} -t ${aws_dynamodb_table.ami_tracker.name} -d ${path.cwd}/tracked_images EOT working_dir = "${path.module}/scripts" } diff --git a/terraform/iam.tf b/terraform/iam.tf index 2a9f624..780bda0 100644 --- a/terraform/iam.tf +++ b/terraform/iam.tf @@ -1,6 +1,6 @@ resource "aws_iam_role" "ami_tracker_queuer_role" { - name = "ami-tracker-queuer" + name = "${var.app_name}-queuer" force_detach_policies = true assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -17,7 +17,7 @@ resource "aws_iam_role" "ami_tracker_queuer_role" { } resource "aws_iam_role_policy" "ami_tracker_queuer_policy" { - name = "ami-tracker-queuer-policy" + name = "${var.app_name}-queuer-policy" role = aws_iam_role.ami_tracker_queuer_role.id policy = jsonencode({ Version = "2012-10-17" @@ -64,7 +64,7 @@ resource "aws_iam_role_policy" "ami_tracker_queuer_policy" { } resource "aws_iam_role" "ami_tracker_lookup_role" { - name = "ami-tracker-lookup" + name = "${var.app_name}-lookup" force_detach_policies = true assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -81,7 +81,7 @@ resource "aws_iam_role" "ami_tracker_lookup_role" { } resource "aws_iam_role_policy" "ami_tracker_lookup_policy" { - name = "ami-tracker-lookup-policy" + name = "${var.app_name}-lookup-policy" role = aws_iam_role.ami_tracker_lookup_role.id policy = jsonencode({ Version = "2012-10-17" diff --git a/terraform/lambda.tf b/terraform/lambda.tf index a73630b..45e633b 100644 --- a/terraform/lambda.tf +++ b/terraform/lambda.tf @@ -33,7 +33,7 @@ resource "aws_lambda_function" "ami_tracker_queuer" { timeout = 180 memory_size = 150 source_code_hash = data.archive_file.ami_tracker_queuer_lambda.output_base64sha256 - layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:26"] + #layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:26"] runtime = "python3.9" dynamic "environment" { @@ -66,7 +66,7 @@ resource "aws_lambda_function" "ami_tracker_lookup" { timeout = 180 memory_size = 150 source_code_hash = data.archive_file.ami_tracker_lookup_lambda.output_base64sha256 - layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:26"] + #layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:26"] runtime = "python3.9" dynamic "environment" { for_each = length(keys(local.lookup_env)) == 0 ? [] : [true] diff --git a/terraform/scripts/sync_config.py b/terraform/scripts/sync_config.py index d49bf58..bd728c5 100755 --- a/terraform/scripts/sync_config.py +++ b/terraform/scripts/sync_config.py @@ -10,6 +10,9 @@ help='Region where your Dynamodb table resides', required=True) p.add_argument('-t', '--table_name', dest='table_name', action='store', default='AmiTracker', help='Name of the DynamoDB table to store lookup config', required=True) +p.add_argument('-d', '--images_dir', dest='images_dir', action='store', default='./tracked_images', + help='Directory where tracked images definitions are stored', required=True) + logger = logging.getLogger() logger.setLevel(logging.INFO) @@ -17,7 +20,7 @@ def load_yaml_config(): tracked_images_yaml = [] - with os.scandir('../../tracked_images') as images_config: + with os.scandir(args.images_dir) as images_config: for entry in images_config: if entry.name.endswith('.yml') and entry.is_file(): with open(f'{entry.path}') as f: @@ -141,6 +144,8 @@ def delete_config(table, deleted_images): def main(**kwargs): + print(args.images_dir) + print(os.path.abspath(args.images_dir)) ddb_resource = boto3.resource('dynamodb', region_name=args.region) diff --git a/terraform/sns.tf b/terraform/sns.tf index 77cd24b..a263a7e 100644 --- a/terraform/sns.tf +++ b/terraform/sns.tf @@ -1,5 +1,5 @@ locals { - ami_tracker_sns_key_alias = "alias/ami-tracker-sns-key" + ami_tracker_sns_key_alias = "alias/${var.app_name}-sns-key" } resource "aws_kms_key" "ami_tracker_sns_key" { From aed08c6620d8dc34d0594d6259cf7ecb7bbf908a Mon Sep 17 00:00:00 2001 From: Ryan Lafferty <1731734+lafferrs@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:34:37 -0400 Subject: [PATCH 3/8] ami --- README.md | 6 +++--- example/main.tf | 2 +- terraform/lambda.tf | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ccf1c20..246a5d4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# AMI Version Tracker +# Amazon Machine Image Version Tracker ## Purpose -Once deployed to your AWS Account, the resources and associated yaml configuration in this terraform module can be used to track and alert on new versions of AWS and third-party AMIs. This allows for eventing on AMI releases, so you can run a build / configuration processes or act to upgrade your development environment when new upstream AMIs are released. +Once deployed to your [Amazon Web Services](https://aws.amazon.com) Account, the resources and associated yaml configuration in this terraform module can be used to track and alert on new versions of AWS and third-party Amazon Machine Images. This allows for eventing on AMI releases, so you can run a build / configuration processes or act to upgrade your development environment when new upstream AMIs are released. For example, this project can serve to fill the gap in notifications of new EKS optimized AMIs as requested here: https://github.com/aws/containers-roadmap/issues/734 @@ -10,7 +10,7 @@ For example, this project can serve to fill the gap in notifications of new EKS ## Prerequisites -An [Amazon Web Services Account](https://aws.amazon.com) and some basic knowledge of how [Terraform Modules](https://developer.hashicorp.com/terraform/language/modules) work are required to get started with this project. +An [Amazon Web Services](https://aws.amazon.com) Account and some basic knowledge of how [Terraform Modules](https://developer.hashicorp.com/terraform/language/modules) work are required to get started with this project. ## Deploying to your AWS Account diff --git a/example/main.tf b/example/main.tf index b076031..11f33cb 100644 --- a/example/main.tf +++ b/example/main.tf @@ -1,7 +1,7 @@ module "ami-tracker" { source = "../terraform" - app_name = "AMI-Tracker-Thingy" + app_name = "AmiTrackerExample" environment = "dev" deploy_alarms = true deploy_alarm_sns = true diff --git a/terraform/lambda.tf b/terraform/lambda.tf index 45e633b..5d8c065 100644 --- a/terraform/lambda.tf +++ b/terraform/lambda.tf @@ -33,7 +33,7 @@ resource "aws_lambda_function" "ami_tracker_queuer" { timeout = 180 memory_size = 150 source_code_hash = data.archive_file.ami_tracker_queuer_lambda.output_base64sha256 - #layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:26"] + #layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:45"] runtime = "python3.9" dynamic "environment" { @@ -66,7 +66,7 @@ resource "aws_lambda_function" "ami_tracker_lookup" { timeout = 180 memory_size = 150 source_code_hash = data.archive_file.ami_tracker_lookup_lambda.output_base64sha256 - #layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:26"] + #layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:45"] runtime = "python3.9" dynamic "environment" { for_each = length(keys(local.lookup_env)) == 0 ? [] : [true] From 79bf318e2e5af7ad6b625e0b902863f8061005f1 Mon Sep 17 00:00:00 2001 From: Ryan Lafferty <1731734+lafferrs@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:37:49 -0400 Subject: [PATCH 4/8] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 246a5d4..473b106 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ An [Amazon Web Services](https://aws.amazon.com) Account and some basic knowledg Please see the [INSTALL](INSTALL.md) document for more information. -## Configuring a tracker +## Configuring a tracked image To track upstream AMI versions, simply add a yaml file to the `tracked_images` folder. There are two configuration types available, Parameter and Filter. From 546ee5ec57b29353c49d6ec0bb70f0bad1edd7ee Mon Sep 17 00:00:00 2001 From: Ryan Lafferty <1731734+lafferrs@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:40:23 -0400 Subject: [PATCH 5/8] readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 473b106..96cccc5 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,6 @@ The solution consists of a DynamoDB table, 2 Lambda functions, an SQS queue, and ## Sample Event ```json ---- "v1": { "Message": "A new version of the amazon-linux-2-eks-1.27 has been released. You are now able to launch new EC2 instances from these AMIs.", "image": { From f6e2ca24a1fb24cc4104f44982ea6394fc2694cb Mon Sep 17 00:00:00 2001 From: Ryan Lafferty <1731734+lafferrs@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:58:45 -0400 Subject: [PATCH 6/8] refer to github project --- example/main.tf | 2 +- terraform/lambda.tf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/main.tf b/example/main.tf index 11f33cb..a98e73a 100644 --- a/example/main.tf +++ b/example/main.tf @@ -1,5 +1,5 @@ module "ami-tracker" { - source = "../terraform" + source = "git::https://github.com/Evernorth/aws-ami-tracker.git//terraform?ref=initial" app_name = "AmiTrackerExample" environment = "dev" diff --git a/terraform/lambda.tf b/terraform/lambda.tf index 5d8c065..f5e5546 100644 --- a/terraform/lambda.tf +++ b/terraform/lambda.tf @@ -33,7 +33,7 @@ resource "aws_lambda_function" "ami_tracker_queuer" { timeout = 180 memory_size = 150 source_code_hash = data.archive_file.ami_tracker_queuer_lambda.output_base64sha256 - #layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:45"] + layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:45"] runtime = "python3.9" dynamic "environment" { @@ -66,7 +66,7 @@ resource "aws_lambda_function" "ami_tracker_lookup" { timeout = 180 memory_size = 150 source_code_hash = data.archive_file.ami_tracker_lookup_lambda.output_base64sha256 - #layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:45"] + layers = ["arn:aws:lambda:${var.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:45"] runtime = "python3.9" dynamic "environment" { for_each = length(keys(local.lookup_env)) == 0 ? [] : [true] From 9c8706a107ed06428748e7e33c786f190c767ce2 Mon Sep 17 00:00:00 2001 From: Ryan Lafferty <1731734+lafferrs@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:01:32 -0400 Subject: [PATCH 7/8] install --- INSTALL.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index a7aa523..284627c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,3 +1,16 @@ -# Installation instructions +# Installation / Usage Instructions -Detailed instructions on how to install, configure, and get the project running. If the instructions are minimal, they can reside in the Readme Installation section. \ No newline at end of file +## Machine / Container Image Prequisites + +The following packages should be installed on your development machine or CI/CD runner. + +- Amazon Web Services Account (with CLI access) +- Terraform version 1.3 or later +- Python 3.9 or later (aliased to python3) +- Python Packages + - boto3 + - pyyaml + +## Example + +Please see the [example/main.tf](example terraform file) in the examples directory for an easy set-up. Once you have your local terraform and provider configured properly, you can simply run `terraform init` followed by `terraform apply` to deploy this solution to your AWS account. \ No newline at end of file From f273747328176c9f01f8194e41a36bdbf8209a81 Mon Sep 17 00:00:00 2001 From: Ryan Lafferty <1731734+lafferrs@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:03:54 -0400 Subject: [PATCH 8/8] install --- INSTALL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 284627c..bff2b46 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -13,4 +13,6 @@ The following packages should be installed on your development machine or CI/CD ## Example +To reference this project, create a `module` block in your terraform code and refer to this module by setting the source to `"git::https://github.com/Evernorth/aws-ami-tracker.git//terraform?ref=main"`. + Please see the [example/main.tf](example terraform file) in the examples directory for an easy set-up. Once you have your local terraform and provider configured properly, you can simply run `terraform init` followed by `terraform apply` to deploy this solution to your AWS account. \ No newline at end of file