├── .github └── PULL_REQUEST_TEMPLATE.md ├── LICENSE ├── NOTICE ├── README.md ├── budgetMaster └── lambda_function.py ├── cfn_budget_lambda_blog_post.json ├── cfn_create_ec2_instances.json ├── createBudgetLambda ├── createBudgetLambda.py └── requirements.txt └── removeCreateInstancePermissions └── lambda_function.py /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Serverless Automated Cost Controls 2 | Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Automated Cost Controls 2 | 3 | Sample implementation of serverless automated cost controls using AWS Budgets, Amazon SNS and AWS Lambda. 4 | 5 | A companion AWS blog post, titled "Serverless Automated Cost Controls" ,for this repository can be found in the AWS Serverless Blog. 6 | 7 | ## Setup 8 | cd createBudgetLambda 9 | pip install -r requirements.txt -t . 10 | 11 | ## Instructions 12 | please follow the instructions in the section titled "Implementing the solution" in the campanion blog post. 13 | 14 | ## LICENSE SUMMARY 15 | This sample code is made available under a modified MIT license. See the LICENSE file. 16 | -------------------------------------------------------------------------------- /budgetMaster/lambda_function.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | 17 | from __future__ import print_function 18 | 19 | import json 20 | import boto3 21 | import os 22 | from botocore.exceptions import ClientError 23 | 24 | # lambda function that receives the Budget Notification and subsequently 25 | # triggers the action lambda (via step functions) 26 | # Input parameters: 27 | # Notification from Budget 28 | # Environment Variable: StateMachineArn 29 | # Environment Variable: GroupName 30 | # Environment Variable: DetachPolicyArn 31 | # Environment Variable: AttachPolicyArn 32 | # This function starts the execution of the 33 | # configured Step Function State Machine. 34 | 35 | def lambda_handler(event, context): 36 | 37 | try: 38 | #budgetNotification = event['Records'][0]['Sns']['Message'] 39 | #print("Notification from Budget (via SNS): " + budgetNotification) 40 | 41 | sfnClient = boto3.client('stepfunctions') 42 | 43 | inputs = {} 44 | inputs['AttachPolicyArn']=os.environ['AttachPolicyArn'] 45 | inputs['DetachPolicyArn']=os.environ['DetachPolicyArn'] 46 | inputs['GroupName'] = os.environ['GroupName'] 47 | 48 | print (json.dumps(inputs)) 49 | 50 | stateMachineArn = os.environ['StateMachineArn'] 51 | response = sfnClient.start_execution( 52 | stateMachineArn=stateMachineArn, 53 | input=json.dumps(inputs) 54 | ) 55 | 56 | print("Step Function Message ID: " + response['executionArn']) 57 | 58 | return 200 59 | except ClientError as e: 60 | print(e.response['Error']['Code']) 61 | if e.response['Error']['Code'] == 'NotFound': 62 | print("Incorrect Topic Arn. Could not find this topic") 63 | return e.response['ResponseMetadata']['HTTPStatusCode'] 64 | else: 65 | print("Unexpected error: %s" % e) 66 | return 500 67 | -------------------------------------------------------------------------------- /cfn_budget_lambda_blog_post.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion" : "2010-09-09", 3 | 4 | "Description" : "AWS CloudFormation Template that spins up the solution stack pertaining to blog1 in the Serverless Cost Controls series. This creates IAM users, groups, roles, lambda functions, sns topics and budgets. Note that you will need to specify the CAPABILITY_IAM flag when you create the stack to allow this template to execute. You can do this through the AWS management console by clicking on the check box acknowledging that you understand this template creates IAM resources or by specifying the CAPABILITY_IAM flag to the cfn-create-stack command line tool or CreateStack API call. ", 5 | "Resources" : { 6 | 7 | "testUserForProjectBeta" : { 8 | "Type" : "AWS::IAM::User", 9 | "Properties" :{ 10 | "LoginProfile": {"Password": "costContr01s"} 11 | } 12 | }, 13 | 14 | "projectBeta" : { 15 | "Type" : "AWS::IAM::Group", 16 | "Properties" : { 17 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AmazonEC2FullAccess"], 18 | "Policies": [{ 19 | "PolicyName": "root", 20 | "PolicyDocument": { 21 | "Version": "2012-10-17", 22 | "Statement": [ 23 | { 24 | "Effect": "Allow", 25 | "Action": [ 26 | "cloudformation:DescribeStacks", 27 | "cloudformation:DescribeStackEvents", 28 | "cloudformation:DescribeStackResource", 29 | "cloudformation:DescribeStackResources", 30 | "cloudformation:GetTemplate", 31 | "cloudformation:GetTemplateSummary", 32 | "cloudformation:CreateStack", 33 | "cloudformation:UpdateStack", 34 | "cloudformation:List*", 35 | "cloudformation:DeleteStack" 36 | ], 37 | "Resource": "*", 38 | "Condition" : { "ForAllValues:StringEquals" : { 39 | "cloudformation:TemplateUrl" : ["https://s3-us-west-2.amazonaws.com/serverless-automated-cost-controls/cfn_create_ec2_instances.json"] 40 | } 41 | } 42 | } 43 | ] 44 | } 45 | }] 46 | } 47 | }, 48 | 49 | "budgetNotificationTopic" : { 50 | "Type" : "AWS::SNS::Topic" 51 | }, 52 | 53 | "budgetNotificationTopicPolicy": { 54 | "Type" : "AWS::SNS::TopicPolicy", 55 | "Properties" : 56 | { 57 | "PolicyDocument": { 58 | "Id" : "MyTopicPolicy", 59 | "Version" : "2012-10-17", 60 | "Statement" : [{ 61 | "Sid": "AWSBudgets-notification-1", 62 | "Effect": "Allow", 63 | "Principal": { 64 | "Service": "budgets.amazonaws.com" 65 | }, 66 | "Action": "SNS:Publish", 67 | "Resource": {"Ref":"budgetNotificationTopic"} 68 | }] 69 | }, 70 | "Topics" : [ 71 | {"Ref":"budgetNotificationTopic"} 72 | ] 73 | } 74 | }, 75 | "MasterLambdaSubscription" : { 76 | "Type" : "AWS::SNS::Subscription", 77 | "Properties" : { 78 | "Endpoint" :{ "Fn::GetAtt" : ["budgetMasterLambda", "Arn"] }, 79 | "Protocol" : "lambda", 80 | "TopicArn" : {"Fn::Join": ["",[ 81 | "arn:aws:sns:",{"Ref" : "AWS::Region"},":", 82 | {"Ref" : "AWS::AccountId"}, 83 | ":", 84 | {"Fn::GetAtt":["budgetNotificationTopic", "TopicName"]} 85 | ]] 86 | } 87 | }, 88 | "DependsOn" : ["budgetNotificationTopic", "budgetMasterLambda"] 89 | }, 90 | 91 | "budgetMasterLambda": { 92 | "Type": "AWS::Lambda::Function", 93 | "Properties": { 94 | "Environment": { 95 | "Variables":{ 96 | "GroupName" : { "Ref" : "projectBeta" }, 97 | "DetachPolicyArn" : "arn:aws:iam::aws:policy/AmazonEC2FullAccess", 98 | "AttachPolicyArn" : "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess", 99 | "StateMachineArn": { "Ref": "budgetActionSM"} 100 | }}, 101 | "Handler": "lambda_function.lambda_handler", 102 | "Role": { "Fn::GetAtt" : ["LambdaSnsRole", "Arn"] }, 103 | "Code": { 104 | "S3Bucket": {"Fn::Join":["",["serverless-automated-cost-controls-",{"Ref" : "AWS::Region"}]]}, 105 | "S3Key": "budgetMaster.zip" 106 | }, 107 | "Runtime": "python2.7", 108 | "Timeout": "25" 109 | } 110 | }, 111 | 112 | "budgetMasterLambdaInvokePermission": { 113 | "Type": "AWS::Lambda::Permission", 114 | "Properties": { 115 | "Action": "lambda:InvokeFunction", 116 | "Principal": "sns.amazonaws.com", 117 | "SourceArn": { "Ref": "budgetNotificationTopic" }, 118 | "FunctionName": { 119 | "Fn::GetAtt": [ "budgetMasterLambda", "Arn" ] 120 | } 121 | } 122 | }, 123 | 124 | "actionLambda": { 125 | "Type": "AWS::Lambda::Function", 126 | "Properties": { 127 | "Handler": "lambda_function.lambda_handler", 128 | "Role": { "Fn::GetAtt" : ["LambdaIamRole", "Arn"] }, 129 | "Code": { 130 | "S3Bucket": {"Fn::Join":["",["serverless-automated-cost-controls-",{"Ref" : "AWS::Region"}]]}, 131 | "S3Key": "removeCreateInstancePermissions.zip" 132 | }, 133 | "Runtime": "python2.7", 134 | "Timeout": "25" 135 | } 136 | }, 137 | 138 | "createBudgetLambda": { 139 | "Type": "AWS::Lambda::Function", 140 | "Properties": { 141 | "Environment": { "Variables" : {"AccountId":{ "Ref" : "AWS::AccountId" }, 142 | "BudgetNotificationArn": {"Fn::Join": ["",["arn:aws:sns:",{"Ref" : "AWS::Region"},":",{"Ref" : "AWS::AccountId"},":", 143 | {"Fn::GetAtt" : ["budgetNotificationTopic", "TopicName"]}]]}} }, 144 | "Handler": "createBudgetLambda.lambda_handler", 145 | "Role": { "Fn::GetAtt" : ["LambdaBudgetRole", "Arn"] }, 146 | "Code": { 147 | "S3Bucket": {"Fn::Join":["",["serverless-automated-cost-controls-",{"Ref" : "AWS::Region"}]]}, 148 | "S3Key": "createBudgetLambda.zip" 149 | }, 150 | "Runtime": "python2.7", 151 | "Timeout": "25" 152 | }, 153 | "DependsOn" : "budgetNotificationTopic" 154 | }, 155 | 156 | "LambdaBudgetRole": { 157 | "Type": "AWS::IAM::Role", 158 | "Properties": { 159 | "AssumeRolePolicyDocument": { 160 | "Version": "2012-10-17", 161 | "Statement": [{ "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com"]}, "Action": ["sts:AssumeRole"] }] 162 | }, 163 | "Path": "/", 164 | "Policies": [{ 165 | "PolicyName": "root", 166 | "PolicyDocument": { 167 | "Version": "2012-10-17", 168 | "Statement": [ 169 | { 170 | "Effect": "Allow", 171 | "Action": [ 172 | "logs:CreateLogGroup", 173 | "logs:CreateLogStream", 174 | "logs:PutLogEvents" 175 | ], 176 | "Resource": "*" 177 | }, 178 | { 179 | "Sid": "Stmt1435216493000", 180 | "Effect": "Allow", 181 | "Action": [ 182 | "aws-portal:ViewBilling", 183 | "aws-portal:ModifyBilling", 184 | "budgets:ViewBudget", 185 | "budgets:ModifyBudget" 186 | ], 187 | "Resource": [ 188 | "*" 189 | ] 190 | }, 191 | { 192 | "Sid": "Stmt1435216514000", 193 | "Effect": "Allow", 194 | "Action": [ 195 | "cloudwatch:*" 196 | ], 197 | "Resource": [ 198 | "*" 199 | ] 200 | }, 201 | { 202 | "Sid": "Stmt1435216552000", 203 | "Effect": "Allow", 204 | "Action": [ 205 | "sns:*" 206 | ], 207 | "Resource": [ 208 | {"Fn::Join" : ["",["arn:aws:sns:",{"Ref" : "AWS::Region"}]]} 209 | ] 210 | } 211 | ] 212 | } 213 | }] 214 | } 215 | }, 216 | 217 | "LambdaSnsRole": { 218 | "Type": "AWS::IAM::Role", 219 | "Properties": { 220 | "AssumeRolePolicyDocument": { 221 | "Version": "2012-10-17", 222 | "Statement": [{ "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com"]}, "Action": ["sts:AssumeRole"] }] 223 | }, 224 | "Path": "/", 225 | "Policies": [{ 226 | "PolicyName": "root", 227 | "PolicyDocument": { 228 | "Version": "2012-10-17", 229 | "Statement": [ 230 | { 231 | "Effect": "Allow", 232 | "Action": [ 233 | "logs:CreateLogGroup", 234 | "logs:CreateLogStream", 235 | "logs:PutLogEvents" 236 | ], 237 | "Resource": "*" 238 | } 239 | ] 240 | } 241 | }], 242 | "ManagedPolicyArns":[ 243 | "arn:aws:iam::aws:policy/AWSStepFunctionsFullAccess" 244 | ] 245 | } 246 | }, 247 | 248 | "LambdaIamRole": { 249 | "Type": "AWS::IAM::Role", 250 | "Properties": { 251 | "AssumeRolePolicyDocument": { 252 | "Version": "2012-10-17", 253 | "Statement": [{ "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com"]}, "Action": ["sts:AssumeRole"] }] 254 | }, 255 | "Path": "/", 256 | "Policies": [{ 257 | "PolicyName": "root", 258 | "PolicyDocument": { 259 | "Version": "2012-10-17", 260 | "Statement": [ 261 | { 262 | "Effect": "Allow", 263 | "Action": [ 264 | "logs:CreateLogGroup", 265 | "logs:CreateLogStream", 266 | "logs:PutLogEvents" 267 | ], 268 | "Resource": "*" 269 | }, 270 | { 271 | "Action": [ 272 | "iam:*" 273 | ], 274 | "Effect": "Allow", 275 | "Resource": "*" 276 | } 277 | ] 278 | } 279 | }] 280 | } 281 | }, 282 | 283 | "Users" : { 284 | "Type" : "AWS::IAM::UserToGroupAddition", 285 | "Properties" : { 286 | "GroupName": { "Ref" : "projectBeta" }, 287 | "Users" : [ { "Ref" : "testUserForProjectBeta" } ] 288 | } 289 | }, 290 | 291 | 292 | "CreateBudget": { 293 | "Type": "Custom::CreateBudget", 294 | "Properties": { 295 | "ServiceToken": { "Fn::GetAtt" : ["createBudgetLambda", "Arn"] } 296 | }, 297 | "DependsOn" : "createBudgetLambda" 298 | }, 299 | 300 | "BudgetActionStepFNRole" : { 301 | "Type": "AWS::IAM::Role", 302 | "Properties": { 303 | "Path": "/service-role/", 304 | "AssumeRolePolicyDocument": { 305 | "Version": "2012-10-17", 306 | "Statement": [{ 307 | "Effect": "Allow", 308 | "Principal": {"Service": [ 309 | {"Fn::Join":[ 310 | "", 311 | ["states.",{"Ref" : "AWS::Region"},".amazonaws.com"]]} 312 | ]}, 313 | "Action": ["sts:AssumeRole"] 314 | }] 315 | }, 316 | "Policies" : [{ 317 | "PolicyName": "allowLambdaInvoke", 318 | "PolicyDocument": { 319 | "Version": "2012-10-17", 320 | "Statement": [{ 321 | "Effect": "Allow", 322 | "Action": [ 323 | "lambda:InvokeFunction" 324 | ], 325 | "Resource": "*" 326 | }] 327 | } 328 | }, 329 | { 330 | "PolicyName":"allowStepFuns", 331 | "PolicyDocument" :{ 332 | "Version": "2012-10-17", 333 | "Statement":[{ 334 | "Effect": "Allow", 335 | "Action": [ 336 | "states:*" 337 | ], 338 | "Resource": [ 339 | "*" 340 | ] 341 | } 342 | ] 343 | } 344 | }] 345 | } 346 | 347 | }, 348 | 349 | "budgetActionSM" : { 350 | "Type": "AWS::StepFunctions::StateMachine", 351 | "Properties": { 352 | "DefinitionString": { 353 | "Fn::Join": [ 354 | "", 355 | [ 356 | "{\"Comment\": \"Removes Permissions to create more resources\", \"StartAt\": \"RemovePermissions\", \"States\": {\"RemovePermissions\": {\"Type\": \"Task\",\"Resource\":\"", 357 | { "Fn::GetAtt" : ["actionLambda", "Arn"] }, 358 | "\",\"End\": true}}}" 359 | ] 360 | ] 361 | }, 362 | "RoleArn": {"Fn::GetAtt" : ["BudgetActionStepFNRole", "Arn"] } 363 | } 364 | } 365 | }, 366 | 367 | 368 | "Outputs" : { 369 | "SignInURL" : { 370 | "Value": {"Fn::Join": ["",["https://",{"Ref" : "AWS::AccountId"},".signin.aws.amazon.com/console/"]]}, 371 | "Description" : "AWS Sign-in URL" 372 | }, 373 | "UserName" : { 374 | "Value": { "Ref": "testUserForProjectBeta" }, 375 | "Description" : "Username to login to the AWS Console for the next part." 376 | }, 377 | "Password" : { 378 | "Value": "costContr01s", 379 | "Description" : "Password to login to the AWS Console for the next part." 380 | }, 381 | "TemplateURL": { 382 | "Value": "https://s3-us-west-2.amazonaws.com/serverless-automated-cost-controls/cfn_create_ec2_instances.json", 383 | "Description" : "Cloudformation Template URL - to create the test Ec2 cluster" 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /cfn_create_ec2_instances.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion" : "2010-09-09", 3 | 4 | "Description" : "AWS CloudFormation Template that spins up the test Ec2 instances (4) pertaining to blog1 in the Serverless Cost Controls series. Create an Amazon EC2 instance running the Amazon Linux AMI. The AMI is chosen based on the region in which the stack is run. Since no action is required on the instance, there are no open ports on these instances. **WARNING** This template creates 4 Amazon EC2 instances. You will be billed for the AWS resources used if you create a stack from this template", 5 | 6 | "Mappings" : { 7 | "AWSInstanceType2Arch" : { 8 | "t1.micro" : { "Arch" : "PV64" }, 9 | "t2.nano" : { "Arch" : "HVM64" }, 10 | "t2.micro" : { "Arch" : "HVM64" }, 11 | "t2.small" : { "Arch" : "HVM64" }, 12 | "t2.medium" : { "Arch" : "HVM64" }, 13 | "t2.large" : { "Arch" : "HVM64" }, 14 | "m1.small" : { "Arch" : "PV64" }, 15 | "m1.medium" : { "Arch" : "PV64" }, 16 | "m1.large" : { "Arch" : "PV64" }, 17 | "m1.xlarge" : { "Arch" : "PV64" }, 18 | "m2.xlarge" : { "Arch" : "PV64" }, 19 | "m2.2xlarge" : { "Arch" : "PV64" }, 20 | "m2.4xlarge" : { "Arch" : "PV64" }, 21 | "m3.medium" : { "Arch" : "HVM64" }, 22 | "m3.large" : { "Arch" : "HVM64" }, 23 | "m3.xlarge" : { "Arch" : "HVM64" }, 24 | "m3.2xlarge" : { "Arch" : "HVM64" }, 25 | "m4.large" : { "Arch" : "HVM64" }, 26 | "m4.xlarge" : { "Arch" : "HVM64" }, 27 | "m4.2xlarge" : { "Arch" : "HVM64" }, 28 | "m4.4xlarge" : { "Arch" : "HVM64" }, 29 | "m4.10xlarge" : { "Arch" : "HVM64" }, 30 | "c1.medium" : { "Arch" : "PV64" }, 31 | "c1.xlarge" : { "Arch" : "PV64" }, 32 | "c3.large" : { "Arch" : "HVM64" }, 33 | "c3.xlarge" : { "Arch" : "HVM64" }, 34 | "c3.2xlarge" : { "Arch" : "HVM64" }, 35 | "c3.4xlarge" : { "Arch" : "HVM64" }, 36 | "c3.8xlarge" : { "Arch" : "HVM64" }, 37 | "c4.large" : { "Arch" : "HVM64" }, 38 | "c4.xlarge" : { "Arch" : "HVM64" }, 39 | "c4.2xlarge" : { "Arch" : "HVM64" }, 40 | "c4.4xlarge" : { "Arch" : "HVM64" }, 41 | "c4.8xlarge" : { "Arch" : "HVM64" }, 42 | "g2.2xlarge" : { "Arch" : "HVMG2" }, 43 | "g2.8xlarge" : { "Arch" : "HVMG2" }, 44 | "r3.large" : { "Arch" : "HVM64" }, 45 | "r3.xlarge" : { "Arch" : "HVM64" }, 46 | "r3.2xlarge" : { "Arch" : "HVM64" }, 47 | "r3.4xlarge" : { "Arch" : "HVM64" }, 48 | "r3.8xlarge" : { "Arch" : "HVM64" }, 49 | "i2.xlarge" : { "Arch" : "HVM64" }, 50 | "i2.2xlarge" : { "Arch" : "HVM64" }, 51 | "i2.4xlarge" : { "Arch" : "HVM64" }, 52 | "i2.8xlarge" : { "Arch" : "HVM64" }, 53 | "d2.xlarge" : { "Arch" : "HVM64" }, 54 | "d2.2xlarge" : { "Arch" : "HVM64" }, 55 | "d2.4xlarge" : { "Arch" : "HVM64" }, 56 | "d2.8xlarge" : { "Arch" : "HVM64" }, 57 | "hi1.4xlarge" : { "Arch" : "HVM64" }, 58 | "hs1.8xlarge" : { "Arch" : "HVM64" }, 59 | "cr1.8xlarge" : { "Arch" : "HVM64" }, 60 | "cc2.8xlarge" : { "Arch" : "HVM64" } 61 | }, 62 | 63 | "AWSInstanceType2NATArch" : { 64 | "t1.micro" : { "Arch" : "NATPV64" }, 65 | "t2.nano" : { "Arch" : "NATHVM64" }, 66 | "t2.micro" : { "Arch" : "NATHVM64" }, 67 | "t2.small" : { "Arch" : "NATHVM64" }, 68 | "t2.medium" : { "Arch" : "NATHVM64" }, 69 | "t2.large" : { "Arch" : "NATHVM64" }, 70 | "m1.small" : { "Arch" : "NATPV64" }, 71 | "m1.medium" : { "Arch" : "NATPV64" }, 72 | "m1.large" : { "Arch" : "NATPV64" }, 73 | "m1.xlarge" : { "Arch" : "NATPV64" }, 74 | "m2.xlarge" : { "Arch" : "NATPV64" }, 75 | "m2.2xlarge" : { "Arch" : "NATPV64" }, 76 | "m2.4xlarge" : { "Arch" : "NATPV64" }, 77 | "m3.medium" : { "Arch" : "NATHVM64" }, 78 | "m3.large" : { "Arch" : "NATHVM64" }, 79 | "m3.xlarge" : { "Arch" : "NATHVM64" }, 80 | "m3.2xlarge" : { "Arch" : "NATHVM64" }, 81 | "m4.large" : { "Arch" : "NATHVM64" }, 82 | "m4.xlarge" : { "Arch" : "NATHVM64" }, 83 | "m4.2xlarge" : { "Arch" : "NATHVM64" }, 84 | "m4.4xlarge" : { "Arch" : "NATHVM64" }, 85 | "m4.10xlarge" : { "Arch" : "NATHVM64" }, 86 | "c1.medium" : { "Arch" : "NATPV64" }, 87 | "c1.xlarge" : { "Arch" : "NATPV64" }, 88 | "c3.large" : { "Arch" : "NATHVM64" }, 89 | "c3.xlarge" : { "Arch" : "NATHVM64" }, 90 | "c3.2xlarge" : { "Arch" : "NATHVM64" }, 91 | "c3.4xlarge" : { "Arch" : "NATHVM64" }, 92 | "c3.8xlarge" : { "Arch" : "NATHVM64" }, 93 | "c4.large" : { "Arch" : "NATHVM64" }, 94 | "c4.xlarge" : { "Arch" : "NATHVM64" }, 95 | "c4.2xlarge" : { "Arch" : "NATHVM64" }, 96 | "c4.4xlarge" : { "Arch" : "NATHVM64" }, 97 | "c4.8xlarge" : { "Arch" : "NATHVM64" }, 98 | "g2.2xlarge" : { "Arch" : "NATHVMG2" }, 99 | "g2.8xlarge" : { "Arch" : "NATHVMG2" }, 100 | "r3.large" : { "Arch" : "NATHVM64" }, 101 | "r3.xlarge" : { "Arch" : "NATHVM64" }, 102 | "r3.2xlarge" : { "Arch" : "NATHVM64" }, 103 | "r3.4xlarge" : { "Arch" : "NATHVM64" }, 104 | "r3.8xlarge" : { "Arch" : "NATHVM64" }, 105 | "i2.xlarge" : { "Arch" : "NATHVM64" }, 106 | "i2.2xlarge" : { "Arch" : "NATHVM64" }, 107 | "i2.4xlarge" : { "Arch" : "NATHVM64" }, 108 | "i2.8xlarge" : { "Arch" : "NATHVM64" }, 109 | "d2.xlarge" : { "Arch" : "NATHVM64" }, 110 | "d2.2xlarge" : { "Arch" : "NATHVM64" }, 111 | "d2.4xlarge" : { "Arch" : "NATHVM64" }, 112 | "d2.8xlarge" : { "Arch" : "NATHVM64" }, 113 | "hi1.4xlarge" : { "Arch" : "NATHVM64" }, 114 | "hs1.8xlarge" : { "Arch" : "NATHVM64" }, 115 | "cr1.8xlarge" : { "Arch" : "NATHVM64" }, 116 | "cc2.8xlarge" : { "Arch" : "NATHVM64" } 117 | } 118 | , 119 | "AWSRegionArch2AMI" : { 120 | "us-east-1" : {"PV64" : "ami-2a69aa47", "HVM64" : "ami-6869aa05", "HVMG2" : "ami-61e27177"}, 121 | "us-west-2" : {"PV64" : "ami-7f77b31f", "HVM64" : "ami-7172b611", "HVMG2" : "ami-60aa3700"}, 122 | "us-west-1" : {"PV64" : "ami-a2490dc2", "HVM64" : "ami-31490d51", "HVMG2" : "ami-4b694d2b"}, 123 | "eu-west-1" : {"PV64" : "ami-4cdd453f", "HVM64" : "ami-f9dd458a", "HVMG2" : "ami-2955524f"}, 124 | "eu-west-2" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-886369ec", "HVMG2" : "NOT_SUPPORTED"}, 125 | "eu-central-1" : {"PV64" : "ami-6527cf0a", "HVM64" : "ami-ea26ce85", "HVMG2" : "ami-81ac71ee"}, 126 | "ap-northeast-1" : {"PV64" : "ami-3e42b65f", "HVM64" : "ami-374db956", "HVMG2" : "ami-46220c21"}, 127 | "ap-northeast-2" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-2b408b45", "HVMG2" : "NOT_SUPPORTED"}, 128 | "ap-southeast-1" : {"PV64" : "ami-df9e4cbc", "HVM64" : "ami-a59b49c6", "HVMG2" : "ami-c212aba1"}, 129 | "ap-southeast-2" : {"PV64" : "ami-63351d00", "HVM64" : "ami-dc361ebf", "HVMG2" : "ami-0ad2db69"}, 130 | "ap-south-1" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-ffbdd790", "HVMG2" : "ami-ca3042a5"}, 131 | "us-east-2" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-f6035893", "HVMG2" : "NOT_SUPPORTED"}, 132 | "ca-central-1" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-730ebd17", "HVMG2" : "NOT_SUPPORTED"}, 133 | "sa-east-1" : {"PV64" : "ami-1ad34676", "HVM64" : "ami-6dd04501", "HVMG2" : "NOT_SUPPORTED"}, 134 | "cn-north-1" : {"PV64" : "ami-77559f1a", "HVM64" : "ami-8e6aa0e3", "HVMG2" : "NOT_SUPPORTED"} 135 | } 136 | 137 | }, 138 | 139 | "Resources" : { 140 | "LaunchConfig" : { 141 | "Type" : "AWS::AutoScaling::LaunchConfiguration", 142 | "Properties" : { 143 | "ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" }, 144 | { "Fn::FindInMap" : [ "AWSInstanceType2Arch", "t2.micro", "Arch" ] } ] }, 145 | "InstanceType" : "t2.micro" 146 | } 147 | }, 148 | "TestForCostControlsGroup" : { 149 | "Type" : "AWS::AutoScaling::AutoScalingGroup", 150 | "Properties" : { 151 | "AvailabilityZones" : { "Fn::GetAZs" : "" }, 152 | "LaunchConfigurationName" : { "Ref" : "LaunchConfig" }, 153 | "MinSize" : "4", 154 | "MaxSize" : "4", 155 | "Tags" : [ { 156 | "Key" : "Project", 157 | "Value" : "Beta", 158 | "PropagateAtLaunch" : "true" 159 | }] 160 | } 161 | } 162 | }, 163 | 164 | "Outputs" : { 165 | "InstanceId" : { 166 | "Description" : "InstanceIds of the newly created EC2 instance", 167 | "Value" : { "Ref" : "TestForCostControlsGroup" } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /createBudgetLambda/createBudgetLambda.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | import boto3 17 | import json 18 | import os 19 | import datetime 20 | import requests 21 | import calendar 22 | from botocore.exceptions import ClientError 23 | 24 | # lambda function that interacts creates a budget in response to 25 | # Input parameters: None 26 | # Environment Variable: AccountId - gets set from cfn 27 | # This function creates (and deletes) a budget for Project Beta 28 | def lambda_handler(context,event): 29 | try: 30 | budgetClient = boto3.client('budgets') 31 | sns = boto3.resource('sns') 32 | 33 | responseStatus = 'SUCCESS' 34 | responseData = {} 35 | 36 | 37 | print "first list the existing budgets" 38 | response = budgetClient.describe_budgets(AccountId= os.environ['AccountId'], MaxResults= 99) 39 | print response 40 | 41 | if context['RequestType'] == 'Delete': 42 | # Delete the Budget 43 | print "deleting the budget" 44 | response = budgetClient.delete_budget(AccountId= os.environ['AccountId'],BudgetName= 'Monthly EC2 Budget for Beta') 45 | print response 46 | responseData = {'Success': 'Budget Deleted.'} 47 | sendResponse(event, context, responseStatus, responseData) 48 | 49 | if context['RequestType'] == 'Create': 50 | response = budgetClient.create_budget( 51 | AccountId= os.environ['AccountId'], 52 | Budget={ 53 | 'BudgetName': 'Monthly EC2 Budget for Beta', 54 | 'BudgetLimit': { 55 | 'Amount': '2', 56 | 'Unit': 'USD' 57 | }, 58 | 'CostFilters': { 59 | 'Service': [ 60 | 'Amazon Elastic Compute Cloud - Compute', 61 | ], 62 | 'TagKeyValue': ['user:Project$Beta'] 63 | }, 64 | 'CostTypes': { 65 | 'IncludeTax': True, 66 | 'IncludeSubscription': True, 67 | 'UseBlended': False 68 | }, 69 | 'TimeUnit': 'MONTHLY', 70 | 'TimePeriod': { 71 | 'Start': datetime.datetime(datetime.date.today().year, datetime.date.today().month, calendar.monthrange(datetime.date.today().year,datetime.date.today().month)[0]), 72 | 'End': datetime.datetime(datetime.date.today().year, datetime.date.today().month, calendar.monthrange(datetime.date.today().year,datetime.date.today().month)[1]) 73 | }, 74 | 'CalculatedSpend': { 75 | 'ActualSpend': { 76 | 'Amount': '1.75', 77 | 'Unit': 'USD' 78 | }, 79 | 'ForecastedSpend': { 80 | 'Amount': '1.75', 81 | 'Unit': 'USD' 82 | } 83 | }, 84 | 'BudgetType': 'COST' 85 | }, 86 | NotificationsWithSubscribers=[ 87 | { 88 | 'Notification': { 89 | 'NotificationType': 'ACTUAL', 90 | 'ComparisonOperator': 'GREATER_THAN', 91 | 'Threshold': 1.75 92 | }, 93 | 'Subscribers': [ 94 | { 95 | 'SubscriptionType': 'SNS', 96 | 'Address': os.environ['BudgetNotificationArn'] 97 | }, 98 | ] 99 | } 100 | ] 101 | ) 102 | 103 | print response 104 | responseData = {'Success': 'Budget Created.'} 105 | 106 | sendResponse(event, context, responseStatus, responseData) 107 | 108 | except ClientError as e: 109 | responseStatus = 'FAILED' 110 | responseData = {'Failed': 'Failed to create Budget.'} 111 | if e.response['Error']['Code'] == 'DuplicateRecordException': 112 | print "Just hit a Duplicate Record Exception. Need to talk to the Budgets team about this. Wil proceed, since it does not seem to make any functional diff" 113 | responseStatus = 'SUCCESS' 114 | responseData = {'Success': 'Success depite the duplicate record error!'} 115 | sendResponse(event, context, responseStatus, responseData) 116 | elif e.response['Error']['Code'] == 'NoSuchEntity': 117 | print "Incorrect Policy Arn. Either the policy is not attached to the given group, or the policy arn does not exist" 118 | sendResponse(event, context, responseStatus, responseData) 119 | else: 120 | print "Unexpected error: %s" % e 121 | sendResponse(event, context, responseStatus, responseData) 122 | 123 | 124 | def sendResponse(event, context, responseStatus, responseData): 125 | responseBody = {'Status': responseStatus, 126 | 'Reason': 'See the details in CloudWatch Log Stream ', 127 | 'PhysicalResourceId': context['ServiceToken'], 128 | 'StackId': context['StackId'], 129 | 'RequestId': context['RequestId'], 130 | 'LogicalResourceId': context['LogicalResourceId'], 131 | 'Data': responseData} 132 | print 'RESPONSE BODY:n' + json.dumps(responseBody) 133 | try: 134 | req = requests.put(context['ResponseURL'], data=json.dumps(responseBody)) 135 | if req.status_code != 200: 136 | print req.text 137 | raise Exception('Recieved non 200 response while sending response to CFN.') 138 | return 139 | except requests.exceptions.RequestException as e: 140 | print e 141 | raise 142 | 143 | if __name__ == '__main__': 144 | lambda_handler('event', 'handler') 145 | -------------------------------------------------------------------------------- /createBudgetLambda/requirements.txt: -------------------------------------------------------------------------------- 1 | requests >=2.13 2 | -------------------------------------------------------------------------------- /removeCreateInstancePermissions/lambda_function.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | import boto3 17 | import json 18 | from botocore.exceptions import ClientError 19 | 20 | # lambda function that interacts with IAM 21 | # to replace the managed IAM policy on a given IAM Group. 22 | # Input parameters: 23 | # GroupName = the IAM Group Name 24 | # DetachPolicyArn = the ARN of the policy that needs to be detached. 25 | # AttachPolicyArn = the ARN of the policy that needs to be attached. 26 | # 27 | # The function is triggered from a step function state machine 28 | # The code attempts to first detach the given policy (referenced through its ARN) 29 | # Then it attempts to attach the given policy (again, referenced through its ARN) 30 | 31 | def lambda_handler(context,event): 32 | try: 33 | iam = boto3.resource('iam') 34 | print ("parameters: " + json.dumps(context)) 35 | 36 | group = iam.Group(context['GroupName']) 37 | group.detach_policy(PolicyArn=context['DetachPolicyArn']) 38 | group.attach_policy(PolicyArn=context['AttachPolicyArn']) 39 | 40 | return 200 #SUCCESS 41 | except ClientError as e: 42 | if e.response['Error']['Code'] == 'NoSuchEntity': 43 | print ("Incorrect Policy Arn. Either the policy is not attached to the given group, or the policy arn does not exist") 44 | return e.response['ResponseMetadata']['HTTPStatusCode'] 45 | else: 46 | print ("Unexpected error: %s" % e) 47 | return 500 48 | --------------------------------------------------------------------------------