-
Notifications
You must be signed in to change notification settings - Fork 48
[AWSCORE-511] Add cloud formation template for CUR 2.0 #261
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
Changes from all commits
a67b2fc
5180947
4f4060e
9c89838
3be2c06
7601228
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ## 0.0.1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if this matters: you're using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense, I'll change it to |
||
|
|
||
| Initial version | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # Datadog AWS Cloud Cost Setup (CUR 2.0) | ||
|
|
||
| [](ADDLINK) | ||
|
|
||
| ## Overview | ||
|
|
||
| This template sets up AWS Cloud Cost Management for Datadog using **CUR 2.0 (Data Exports)**. It creates the necessary AWS resources and configures the Datadog integration automatically. | ||
|
|
||
| ## AWS Resources | ||
|
|
||
| This template creates the following: | ||
|
|
||
| - **S3 Bucket** (optional) - For storing cost and usage data | ||
| - Bucket policy allowing `bcm-data-exports.amazonaws.com` to write reports | ||
| - **BCM Data Export** (optional) - CUR 2.0 export with hourly granularity in Parquet format | ||
| - **IAM Policy** - Grants Datadog read access to the S3 bucket and Cost Explorer APIs | ||
| - Attached to your existing Datadog integration role | ||
| - **Lambda Function** - Calls the Datadog API to configure CCM with the data export details | ||
|
|
||
| ## Parameters | ||
|
|
||
| | Parameter | Description | Required | | ||
| |-----------|-------------|----------| | ||
| | `DatadogApiKey` | Datadog API key | Yes | | ||
| | `DatadogAppKey` | Datadog APP key | Yes | | ||
| | `DatadogSite` | Datadog site (e.g., datadoghq.com) | No (default: datadoghq.com) | | ||
| | `CloudCostBucketName` | S3 bucket name for cost reports | Yes | | ||
| | `CloudCostBucketRegion` | AWS region of the S3 bucket | Yes | | ||
| | `CloudCostReportPrefix` | Prefix for report files in S3 | Yes | | ||
| | `CloudCostReportName` | Name of the data export | Yes | | ||
| | `DatadogIntegrationRole` | Name of your Datadog integration IAM role | Yes | | ||
| | `CreateCloudCostReport` | Create a new data export (true/false) | No (default: true) | | ||
| | `CreateCloudCostBucket` | Create a new S3 bucket (true/false) | No (default: true) | | ||
|
|
||
| ## Publishing the Template | ||
|
|
||
| Use the release script to upload the template to an S3 bucket: | ||
|
|
||
| ```sh | ||
| ./release.sh <bucket_name> | ||
| ``` | ||
|
|
||
| Use `--private` to prevent granting public access (useful for testing): | ||
|
|
||
| ```sh | ||
| ./release.sh <bucket_name> --private | ||
| ``` | ||
|
|
||
| The template will be available at `s3://<bucket_name>/aws_cloud_cost_cur2/v0.0.1/main.yaml`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| import json | ||
| import logging | ||
| import urllib.request | ||
| import urllib.error | ||
| import cfnresponse | ||
|
|
||
| LOGGER = logging.getLogger() | ||
| LOGGER.setLevel(logging.INFO) | ||
|
|
||
| API_CALL_SOURCE_HEADER_VALUE = "cfn-ccm-cur2" | ||
|
|
||
|
|
||
| def get_datadog_account_uuid(event): | ||
| """Get the Datadog account UUID for this AWS account.""" | ||
| api_key = event["ResourceProperties"]["APIKey"] | ||
| app_key = event["ResourceProperties"]["APPKey"] | ||
| api_url = event["ResourceProperties"]["ApiURL"] | ||
| account_id = event["ResourceProperties"]["AccountId"] | ||
|
|
||
| url = f"https://api.{api_url}/api/v2/integration/aws/accounts?aws_account_id={account_id}" | ||
| headers = { | ||
| "DD-API-KEY": api_key, | ||
| "DD-APPLICATION-KEY": app_key, | ||
| "Dd-Aws-Api-Call-Source": API_CALL_SOURCE_HEADER_VALUE, | ||
| } | ||
| request = urllib.request.Request(url, headers=headers) | ||
| request.get_method = lambda: "GET" | ||
| try: | ||
| response = urllib.request.urlopen(request) | ||
| data = json.loads(response.read()) | ||
| if len(data.get("data", [])) == 0: | ||
| return None, "No Datadog integration found for this AWS account" | ||
| if len(data["data"]) > 1: | ||
| return None, "Multiple Datadog integrations found for this AWS account" | ||
| return data["data"][0]["id"], None | ||
| except urllib.error.HTTPError as e: | ||
| error_body = e.read().decode("utf-8") | ||
| return None, f"Failed to get account: {e.code} - {error_body}" | ||
|
|
||
|
|
||
| def patch_ccm_config(event, uuid): | ||
| """PATCH the Datadog account with CCM data export config.""" | ||
| api_key = event["ResourceProperties"]["APIKey"] | ||
| app_key = event["ResourceProperties"]["APPKey"] | ||
| api_url = event["ResourceProperties"]["ApiURL"] | ||
| bucket_name = event["ResourceProperties"]["BucketName"] | ||
| bucket_region = event["ResourceProperties"]["BucketRegion"] | ||
| report_name = event["ResourceProperties"]["ReportName"] | ||
| report_prefix = event["ResourceProperties"]["ReportPrefix"] | ||
|
|
||
| url = f"https://api.{api_url}/api/v2/integration/aws/accounts/{uuid}" | ||
| headers = { | ||
| "DD-API-KEY": api_key, | ||
| "DD-APPLICATION-KEY": app_key, | ||
| "Dd-Aws-Api-Call-Source": API_CALL_SOURCE_HEADER_VALUE, | ||
| "Content-Type": "application/json", | ||
| "Accept": "application/json", | ||
| } | ||
|
|
||
| payload = { | ||
| "data": { | ||
| "type": "account", | ||
| "attributes": { | ||
| "ccm_config": { | ||
| "data_export_configs": [ | ||
| { | ||
| "report_name": report_name, | ||
| "report_prefix": report_prefix, | ||
| "report_type": "CUR2.0", | ||
| "bucket_name": bucket_name, | ||
| "bucket_region": bucket_region, | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| data = json.dumps(payload).encode("utf-8") | ||
| request = urllib.request.Request(url, data=data, headers=headers) | ||
| request.get_method = lambda: "PATCH" | ||
|
|
||
| try: | ||
| response = urllib.request.urlopen(request) | ||
| return response.getcode(), json.loads(response.read()) | ||
| except urllib.error.HTTPError as e: | ||
| error_body = e.read().decode("utf-8") | ||
| return e.code, {"error": error_body} | ||
|
|
||
|
|
||
| def handler(event, context): | ||
| """Handle Lambda event from AWS CloudFormation.""" | ||
| LOGGER.info(f"Received event: {json.dumps(event)}") | ||
|
|
||
| if event["RequestType"] == "Delete": | ||
| # On delete, we don't remove the CCM config - just succeed | ||
| LOGGER.info("Delete request - no action needed for CCM config") | ||
| cfnresponse.send(event, context, cfnresponse.SUCCESS, {"Message": "Delete successful"}) | ||
| return | ||
|
|
||
| try: | ||
| # Get the Datadog account UUID | ||
| uuid, error = get_datadog_account_uuid(event) | ||
| if error: | ||
| LOGGER.error(f"Failed to get account UUID: {error}") | ||
| cfnresponse.send(event, context, cfnresponse.FAILED, {"Message": error}) | ||
| return | ||
|
|
||
| LOGGER.info(f"Found Datadog account UUID: {uuid}") | ||
|
|
||
| # PATCH the CCM config | ||
| status_code, response_data = patch_ccm_config(event, uuid) | ||
|
|
||
| if status_code == 200: | ||
| LOGGER.info("Successfully configured CCM data export") | ||
| cfnresponse.send(event, context, cfnresponse.SUCCESS, { | ||
| "Message": "CCM data export configured successfully", | ||
| "AccountUUID": uuid, | ||
| }) | ||
| else: | ||
| error_msg = f"API returned {status_code}: {response_data}" | ||
| LOGGER.error(error_msg) | ||
| cfnresponse.send(event, context, cfnresponse.FAILED, {"Message": error_msg}) | ||
|
|
||
| except Exception as e: | ||
| LOGGER.exception("Exception during processing") | ||
| cfnresponse.send(event, context, cfnresponse.FAILED, {"Message": str(e)}) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nit: Since the repo is for cloudformation mentioning aws in the path doesn't bring much