From e59061797a01d0ae7d5ec428d25ef1042c36008c Mon Sep 17 00:00:00 2001 From: Eric Kascic Date: Thu, 3 Mar 2016 17:18:43 +0000 Subject: [PATCH] throw a bunch of crap at the wall and see what sticks!!! --- bin/converge_lambda_stack.rb | 14 ++++ bin/subscribe_to_ipchanges.rb | 9 +++ bin/switch_lambda_alias.rb | 13 ++++ cfn/lambda_cfndsl.rb | 5 +- circle.yml | 10 +++ lib/lambda_alias_switcher.rb | 29 ++++++++ pipeline/blue-green-lambda.sh | 13 ++++ pipeline/circleci_deployment.sh | 13 ++++ pipeline/deploy-new-lambda.sh | 20 ++++++ pipeline/store-distro.sh | 41 +++++++++++ spec/blue_green_lambda_spec.rb | 64 +++++++++++++++++ .../basic_lambda_cfndsl.rb | 68 +++++++++++++++++++ 12 files changed, 297 insertions(+), 2 deletions(-) create mode 100755 bin/converge_lambda_stack.rb create mode 100755 bin/subscribe_to_ipchanges.rb create mode 100755 bin/switch_lambda_alias.rb create mode 100644 circle.yml create mode 100644 lib/lambda_alias_switcher.rb create mode 100644 pipeline/blue-green-lambda.sh create mode 100644 pipeline/circleci_deployment.sh create mode 100644 pipeline/deploy-new-lambda.sh create mode 100644 pipeline/store-distro.sh create mode 100644 spec/blue_green_lambda_spec.rb create mode 100644 spec/cfndsl_test_templates/basic_lambda_cfndsl.rb diff --git a/bin/converge_lambda_stack.rb b/bin/converge_lambda_stack.rb new file mode 100755 index 0000000..77833cb --- /dev/null +++ b/bin/converge_lambda_stack.rb @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +require 'trollop' +require_relative 'lib/cloudformation_converger' +require_relative 'lib/ip_space_changed_subscriber' + +opts = Trollop::options do + opt :stack_name, '', type: :string, required: true + opt :path, '', type: :string, required: true +end + +outputs = CloudformationConverger.new.converge stack_name: opts[:stack_name], + stack_path: opts[:path] + +puts outputs['functionname'] \ No newline at end of file diff --git a/bin/subscribe_to_ipchanges.rb b/bin/subscribe_to_ipchanges.rb new file mode 100755 index 0000000..94d19e4 --- /dev/null +++ b/bin/subscribe_to_ipchanges.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +require 'trollop' +require_relative 'lib/ip_space_changed_subscriber' + +opts = Trollop::options do + opt :arn, '', type: :string, required: true +end + +IpSpaceChangedSubscriber.new.subscribe opts[:arn] \ No newline at end of file diff --git a/bin/switch_lambda_alias.rb b/bin/switch_lambda_alias.rb new file mode 100755 index 0000000..84a82e7 --- /dev/null +++ b/bin/switch_lambda_alias.rb @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby +require 'trollop' +require_relative 'lib/blue_green_lambda' + +opts = Trollop::options do + opt :function_name, '', type: :string, required: true + opt :alias, '', type: :string, required: true +end + +alias_arn = LambdaAliasSwitcher.new.switch_alias_of_latest function_name: opts[:stack_name], + alias_arg: opts[:alias] +puts alias_arn + diff --git a/cfn/lambda_cfndsl.rb b/cfn/lambda_cfndsl.rb index 9788908..e67520a 100644 --- a/cfn/lambda_cfndsl.rb +++ b/cfn/lambda_cfndsl.rb @@ -10,8 +10,7 @@ Role FnGetAtt('lambdaExecutionRole', 'Arn') Code({ 'S3Bucket' => 'stelligent-binary-artifact-repo', - 'S3Key' => 'cloudfront-only-security-group-1.0.0-SNAPSHOT.jar', - 'S3ObjectVersion' => '5FBTRF1DLbOfFFO0DbNyR3ptFzjCNWRc' + 'S3Key' => 'cloudfront-only-security-group-1.0.0-SNAPSHOT.jar' }) } @@ -87,4 +86,6 @@ Output(:arn, FnGetAtt('sgUpdaterLambdaFunction', 'Arn')) + Output(:functionname, + Ref('sgUpdaterLambdaFunction')) } \ No newline at end of file diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..9bd1b14 --- /dev/null +++ b/circle.yml @@ -0,0 +1,10 @@ +machine: + ruby: + version: 'jruby-9.0.4.0' + +deployment: + s3: + branch: master + commands: + - pipeline/circleci_deployment.sh + diff --git a/lib/lambda_alias_switcher.rb b/lib/lambda_alias_switcher.rb new file mode 100644 index 0000000..65edf05 --- /dev/null +++ b/lib/lambda_alias_switcher.rb @@ -0,0 +1,29 @@ +require 'aws-sdk' + +class LambdaAliasSwitcher + + def switch_alias_of_latest(function_name:, + alias_arg:) + + client = Aws::Lambda::Client.new + + list_aliases_response = client.list_aliases function_name: function_name + + found_alias = list_aliases_response.aliases.find do |alias_iter| + alias_iter.name == alias_arg + end + + if found_alias.nil? + create_alias_response = client.create_alias function_name: function_name, + name: alias_arg, + function_version: '$LATEST' + + create_alias_response.alias_arn + else + update_alias_response = client.update_alias function_name: function_name, + name: alias_arg, + function_version: '$LATEST' + update_alias_response.alias_arn + end + end +end \ No newline at end of file diff --git a/pipeline/blue-green-lambda.sh b/pipeline/blue-green-lambda.sh new file mode 100644 index 0000000..18476b6 --- /dev/null +++ b/pipeline/blue-green-lambda.sh @@ -0,0 +1,13 @@ +#!/bin/bash -exl + +bin/switch_lambda_alias.rb --function-name ${version_arn} \ + --alias PROD + +# this is idempotent. only needs to run first time after +# lambda function passes the smoke test +# more elegant way to do this? +bin/subscribe_to_ipchanges.rb --arn arn:aws:lambda:${AWS_REGION}:${AWS_ACCOUNT_NUMBER}:function:${lambda_function_name}:PROD + +# delete the old version? + + diff --git a/pipeline/circleci_deployment.sh b/pipeline/circleci_deployment.sh new file mode 100644 index 0000000..4580b8a --- /dev/null +++ b/pipeline/circleci_deployment.sh @@ -0,0 +1,13 @@ +#!/bin/bash -exl + +source pipeline/store-distro.sh +#new_version set + +source pipeline/deploy-new-lambda.sh +#lambda_function_name set +#version_arn set + +# run the smoke tests + +#prod_arn +source pipeline/blue-green-lambda.sh \ No newline at end of file diff --git a/pipeline/deploy-new-lambda.sh b/pipeline/deploy-new-lambda.sh new file mode 100644 index 0000000..aaba03e --- /dev/null +++ b/pipeline/deploy-new-lambda.sh @@ -0,0 +1,20 @@ +#!/bin/bash -elx + +region=${AWS_REGION} + +bundle install --frozen + +stack_name=CloudFront-Lambda-Security-Group + +sed -i "s/1.0.0-SNAPSHOT/${new_version}/g" cfn/lambda_cfndsl.rb + +cfndsl cfn/lambda_cfndsl.rb > output.json + +aws cloudformation validate-template --template-body file://output.json \ + --region ${region} + +lambda_function_name=$(bin/converge_lambda_stack.rb --stack-name ${stack_name} \ + --path output.json) + +version_arn=$(aws lambda publish-version --function-name ${lambda_function_name} | jq '.FunctionArn' | tr -d '"') + diff --git a/pipeline/store-distro.sh b/pipeline/store-distro.sh new file mode 100644 index 0000000..367c353 --- /dev/null +++ b/pipeline/store-distro.sh @@ -0,0 +1,41 @@ +#!/bin/bash -ex +set -o pipefail + +git config --global user.email "build@build.com" +git config --global user.name "build" + +current_version=$(ruby -e 'tags=`git tag -l v0\.0\.*`' \ + -e 'p tags.lines.map { |tag| tag.sub(/v0.0./, "").chomp.to_i }.max') + +if [[ ${current_version} == nil ]]; +then + #for issue scraping + current_version=origin + + new_version='0.0.1' +else + new_version=0.0.$((current_version+1)) +fi + +sed -i "s/1.0.0-SNAPSHOT/${new_version}/g" pom.xml + +mvn install + +#on circle ci - head is ambiguous for reasons that i don't grok +#we haven't made the new tag and we can't if we are going to annotate +head=$(git log -n 1 --oneline | awk '{print $1}') + +echo "Remember! You need to start your commit messages with #x, where x is the issue number your commit resolves." +issues=$(git log v0.0.${current_version}..${head} --oneline | awk '{print $2}' | grep '^#' | uniq) + +git tag -a v${new_version} -m "Issues with commits, not necessarily closed: ${issues}" + +git push --tags + +output_jar_name=cloudfront-only-security-group-${new_version}.jar + +upload_result=$(aws s3api put-object --bucket stelligent-binary-artifact-repo \ + --key ${output_jar_name} \ + --body target/${output_jar_name}) + +echo upload_result=${upload_result} \ No newline at end of file diff --git a/spec/blue_green_lambda_spec.rb b/spec/blue_green_lambda_spec.rb new file mode 100644 index 0000000..413e03b --- /dev/null +++ b/spec/blue_green_lambda_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' +require 'blue_green_lambda' + +describe LambdaAliasSwitcher do + + context 'no alias yet exists for lambda function' do + before(:all) do + @stack_name = stack(stack_name: 'basiclambdafortesting', + path_to_stack: 'spec/cfndsl_test_templates/basic_lambda_cfndsl.rb') + @blue_green_lambda = LambdaAliasSwitcher.new + + @aws_account_number = ENV['AWS_ACCOUNT_NUMBER'] + @aws_region = ENV['AWS_REGION'] + end + + + it 'creates a new alias and assigns it to LATEST of lambda function' do + + alias_arn = @blue_green_lambda.switch_alias_of_latest(function_name: stack_outputs['functionname'], + alias_arg: 'PROD') + + expect(alias_arn).to eq "arn:aws:lambda:#{@aws_region}:#{@aws_account_number}:function:#{stack_outputs['functionname']}:PROD" + + get_alias_response = Aws::Lambda::Client.new.get_alias function_name: stack_outputs['functionname'], + name: 'PROD' + + expect(get_alias_response[:name]).to eq 'PROD' + expect(get_alias_response[:alias_arn]).to eq alias_arn + end + + after(:all) do + cleanup(@stack_name) + end + end + + context 'alias PROD exists for lambda function' do + before(:all) do + @stack_name = stack(stack_name: 'basiclambdafortesting', + path_to_stack: 'spec/cfndsl_test_templates/basic_lambda_cfndsl.rb') + @blue_green_lambda = LambdaAliasSwitcher.new + + @aws_account_number = ENV['AWS_ACCOUNT_NUMBER'] + @aws_region = ENV['AWS_REGION'] + end + + + it 'updates the alias' do + + alias_arn = @blue_green_lambda.switch_alias_of_latest(function_name: stack_outputs[:functionname], + alias_arg: 'PROD') + + alias_arn = @blue_green_lambda.switch_alias_of_latest(function_name: stack_outputs[:functionname], + alias_arg: 'PROD') + + list_aliases_response = Aws::Lambda::Client.new.list_aliases function_name: stack_outputs[:functionname] + + expect(list_aliases_response.aliases.size).to eq 1 + end + + after(:all) do + cleanup(@stack_name) + end + end +end \ No newline at end of file diff --git a/spec/cfndsl_test_templates/basic_lambda_cfndsl.rb b/spec/cfndsl_test_templates/basic_lambda_cfndsl.rb new file mode 100644 index 0000000..c84ac8d --- /dev/null +++ b/spec/cfndsl_test_templates/basic_lambda_cfndsl.rb @@ -0,0 +1,68 @@ +CloudFormation { + Lambda_Function('rBasicLambdaFunction') { + Handler 'myHandler' + Role FnGetAtt('lambdaExecutionRole', 'Arn') + Runtime 'nodejs' + Timeout 240 + MemorySize 128 + Code { + ZipFile FnJoin('', [ + 'exports.myHandler = function(event, context) {', + 'console.log("value1 = " + event.key1);', + 'console.log("value2 = " + event.key2);', + 'context.succeed("some message");' + ]) + } + } + + IAM_Role('lambdaExecutionRole') { + AssumeRolePolicyDocument(JSON.load <<-END + { + "Statement":[ + { + "Action":[ + "sts:AssumeRole" + ], + "Effect":"Allow", + "Principal":{ + "Service":[ + "lambda.amazonaws.com" + ] + } + } + ], + "Version":"2012-10-17" + } + END + ) + + Policies([ + { + 'PolicyName' => 'ReceiveMessagesAndUpdateSecGroups', + 'PolicyDocument' => (JSON.load <<-END + { + "Statement":[ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "arn:aws:logs:*:*:*" + } + ], + "Version":"2012-10-17" + } + END + ) + } + ]) + } + + Output(:arn, + FnGetAtt('rBasicLambdaFunction', 'Arn')) + + Output(:functionname, + Ref('rBasicLambdaFunction')) +} \ No newline at end of file