Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: code for the lambda for exporting account tags #235

Merged
merged 7 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"version": "3.12"
},
"ghcr.io/devcontainers/features/terraform:1": {
"version": "1.1.7",
"version": "1.7.2",
"tflint": "latest",
"terragrunt": "0.36.3"
}
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/tf-apply.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ jobs:
account: 659087519042
role: cds-aws-lz-apply

- account_folder: org_account
module: billing_extract_tags
account: 659087519042
role: cds-aws-lz-apply

- account_folder: log_archive
module: main
account: 274536870005
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/tf-plan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ jobs:
module: sentinel_oidc
account: 659087519042
role: cds-aws-lz-plan
admin_sso_role_arn: ADMIN_SSO_ROLE_ARN

- account_folder: org_account
module: billing_extract_tags
account: 659087519042
role: cds-aws-lz-plan

- account_folder: log_archive
module: main
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ debug.log
creds.sh

.tool-versions
.DS_Store
.DS_Store

__pycache__
44 changes: 44 additions & 0 deletions terragrunt/org_account/billing_extract_tags/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions terragrunt/org_account/billing_extract_tags/eventbridge.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource "aws_cloudwatch_event_rule" "billing_extract_tags" {
name = "billing_extract_tags_daily"
schedule_expression = "cron(0 5 * * ? *)"

tags = local.common_tags
}

resource "aws_cloudwatch_event_target" "billing_extract_tags" {
rule = aws_cloudwatch_event_rule.billing_extract_tags.name
arn = aws_lambda_function.billing_extract_tags.arn
}
80 changes: 80 additions & 0 deletions terragrunt/org_account/billing_extract_tags/iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
resource "aws_iam_role" "billing_extract_tags" {
name = "BillingExtractTags"
assume_role_policy = data.aws_iam_policy_document.billing_extract_tags_assume.json
tags = local.common_tags
}

data "aws_iam_policy_document" "billing_extract_tags_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [
"lambda.amazonaws.com"]
}
}
}

data "aws_iam_policy_document" "billing_extract_tags" {
statement {
effect = "Allow"
actions = ["logs:CreateLogGroup"]
resources = ["arn:aws:logs:${var.region}:${var.account_id}:*"]
}

statement {
effect = "Allow"
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = [
"arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/lambda/billing_extract_tags:*"
]
}

statement {
effect = "Allow"
actions = [
"s3:PutObject*",
"s3:ListBucket",
"s3:GetObject*",
"s3:DeleteObject*",
"s3:GetBucketLocation"
]
resources = [
module.billing_extract_tags.s3_bucket_arn,
"${module.billing_extract_tags.s3_bucket_arn}/*",
]
}
}

resource "aws_iam_policy" "billing_extract_tags" {
name = "BillingExtractTags"
policy = data.aws_iam_policy_document.billing_extract_tags.json
tags = local.common_tags
}

resource "aws_iam_role_policy_attachment" "billing_extract_tags" {
role = aws_iam_role.billing_extract_tags.name
policy_arn = aws_iam_policy.billing_extract_tags.arn
}

data "aws_iam_policy" "org_read_only" {
arn = "arn:aws:iam::aws:policy/AWSOrganizationsReadOnlyAccess"
}

resource "aws_iam_role_policy_attachment" "org_read_only" {
role = aws_iam_role.billing_extract_tags.name
policy_arn = data.aws_iam_policy.org_read_only.arn
}

data "aws_iam_policy" "lambda_insights" {
name = "CloudWatchLambdaInsightsExecutionRolePolicy"
}

resource "aws_iam_role_policy_attachment" "lambda_insights" {
role = aws_iam_role.billing_extract_tags.name
policy_arn = data.aws_iam_policy.lambda_insights.arn
}
44 changes: 44 additions & 0 deletions terragrunt/org_account/billing_extract_tags/lambda.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
data "archive_file" "billing_extract_tags" {
type = "zip"
source_file = "${path.module}/lambdas/billing_extract_tags/main.py"
output_path = "/tmp/main.py.zip"
}

resource "aws_lambda_function" "billing_extract_tags" {
function_name = "billing_extract_tags"
role = aws_iam_role.billing_extract_tags.arn
runtime = "python3.11"
handler = "main.handler"
memory_size = 1024
timeout = 30

filename = data.archive_file.billing_extract_tags.output_path
source_code_hash = filebase64sha256(data.archive_file.billing_extract_tags.output_path)

environment {
variables = {
TARGET_BUCKET = module.billing_extract_tags.s3_bucket_id
}
}

tracing_config {
mode = "PassThrough"
}

tags = local.common_tags
}

resource "aws_lambda_permission" "billing_extract_tags" {
statement_id = "AllowBillingExtractTagsDaily"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.billing_extract_tags.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.billing_extract_tags.arn
}

resource "aws_cloudwatch_log_group" "billing_extract_tags" {
#checkov:skip=CKV_AWS_158:We trust the AWS provided keys
name = "/aws/lambda/${aws_lambda_function.billing_extract_tags.function_name}"
retention_in_days = "14"
tags = local.common_tags
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
default:
python3 main.py

fmt:
black . $(ARGS)

install:
pip3 install --user -r requirements_dev.txt

lint:
flake8 main.py

test:
python -m pytest -s -vv .

.PHONY: \
fmt \
install \
lint \
test
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Get the tags for all accounts in the organization and save them to an s3 bucket.
This is then used to enrich the billing data with the account tags to allow
for business unit filtering.
"""
import json
import logging
import os

import boto3

orgs = boto3.client("organizations")
s3 = boto3.client("s3")

TARGET_BUCKET = os.getenv("TARGET_BUCKET")


def lambda_handler(event, context):
"""
Get the tags for all accounts in the organization and save them to an s3 bucket
"""
logging.info("Getting account tags")
accounts = []
accounts_result = orgs.list_accounts()
accounts += accounts_result["Accounts"]
while "NextToken" in accounts_result:
logging.info("Paginating accounts...")
accounts_result = orgs.list_accounts(NextToken=accounts_result["NextToken"])
accounts += accounts_result["Accounts"]

# Iterate over the accounts and get the tags and then add them to the account in the list
logging.info("Getting account tags")
for account in accounts:
account_tags = orgs.list_tags_for_resource(ResourceId=account["Id"])

# Convert the tags from {'Key': 'Name', 'Value': 'Dev'} to {'Name': 'Dev'}
account_tags["Tags"] = {
tag["Key"]: tag["Value"] for tag in account_tags["Tags"]
}
account["Tags"] = account_tags["Tags"]

# Get a set of all possible tag keys
tag_keys = set()
for account in accounts:
tag_keys.update(account["Tags"].keys())
logging.info(f"Found tag keys: {tag_keys}")

# Add empty strings for all the tags that are not present in the account
logging.info("Adding empty strings for missing tags")
for account in accounts:
for tag_key in tag_keys:
if tag_key not in account["Tags"]:
account["Tags"][tag_key] = ""

# Convert the tags into the format tag_key_name: tag_value and add them to the base object
logging.info("Converting tags to tag_key_name: tag_value")
for account in accounts:
for tag_key, tag_value in account["Tags"].items():
account[f"tag_{tag_key}"] = tag_value
del account["Tags"]

# .write json to string and add a newline between each record
logging.info("Writing account tags to json")
accounts = json.dumps(accounts, default=str)
# accounts = accounts.replace('},', '},\n')
# accounts = accounts.replace('[{', '[\n{')
logging.info(f"Accounts: {accounts}")

# save accounts to an s3 bucket
logging.info("Saving account tags to s3")
s3.put_object(Bucket=TARGET_BUCKET, Key="account_tags.json", Body=accounts)

return {"statusCode": 200}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
boto3==1.34.54
black==23.12.1
flake8==7.0.0
pytest==7.4.4
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import unittest
import os
from unittest.mock import call, patch, MagicMock
from main import lambda_handler

TARGET_BUCKET = "TARGET_BUCKET"
ACCOUNT_TAGS_KEY = "account_tags.json"


class TestLambdaHandler(unittest.TestCase):
def setUp(self):
self.event = {}
self.context = MagicMock()

@patch("main.orgs.list_accounts")
@patch("main.orgs.list_tags_for_resource")
@patch("main.s3.put_object")
@patch("main.TARGET_BUCKET", TARGET_BUCKET)
def test_lambda_handler(
self, mock_s3_put, mock_orgs_list_tags, mock_orgs_list_accounts
):
mock_orgs_list_accounts.return_value = {
"Accounts": [{"Id": "123"}],
}
mock_orgs_list_tags.return_value = {"Tags": [{"Key": "Name", "Value": "Dev"}]}

response = lambda_handler(self.event, self.context)

mock_orgs_list_accounts.assert_called()
mock_orgs_list_tags.assert_called_with(ResourceId="123")
mock_s3_put.assert_called_with(
Bucket=TARGET_BUCKET,
Key=ACCOUNT_TAGS_KEY,
Body='[{"Id": "123", "tag_Name": "Dev"}]',
)

self.assertEqual(response, {"statusCode": 200})

@patch("main.orgs.list_accounts")
@patch("main.orgs.list_tags_for_resource")
@patch("main.s3.put_object")
@patch("main.TARGET_BUCKET", TARGET_BUCKET)
def test_lambda_handler_pagination(
self, mock_s3_put, mock_orgs_list_tags, mock_orgs_list_accounts
):
mock_orgs_list_accounts.side_effect = [
{"Accounts": [{"Id": "123"}], "NextToken": "token"},
{"Accounts": [{"Id": "456"}]},
]
mock_orgs_list_tags.side_effect = [
{"Tags": [{"Key": "Name", "Value": "Dev"}]},
{"Tags": [{"Key": "Name", "Value": "Prod"}]},
]

lambda_handler(self.event, self.context)

mock_orgs_list_accounts.assert_any_call(NextToken="token")
mock_orgs_list_tags.assert_any_call(ResourceId="123")
mock_orgs_list_tags.assert_any_call(ResourceId="456")
mock_s3_put.assert_called_with(
Bucket=TARGET_BUCKET,
Key=ACCOUNT_TAGS_KEY,
Body='[{"Id": "123", "tag_Name": "Dev"}, {"Id": "456", "tag_Name": "Prod"}]',
)
Loading
Loading