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

Feature/dynamo state #49

Open
wants to merge 37 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5bd3d36
-Updated Config class constructor arguments and parameters
jasdbrown Apr 28, 2020
e65f77c
Updated Config class to create the state table if necessary, and upda…
jasdbrown Apr 29, 2020
82d09ac
Adjusted __init__ to use updated Config class
jasdbrown Apr 29, 2020
10bb860
Fixed issue with table creation and checking existence of table
jasdbrown Apr 30, 2020
c6ded8c
Fixed format for updating items
jasdbrown Apr 30, 2020
be4f7cf
Fixed issue with config data formatting
jasdbrown Apr 30, 2020
a58cba0
Fixed issue with retrieving config from the Dynamo table
jasdbrown May 4, 2020
6fbd24d
LambdaPrep class now is passed the sync base and lambda dirs from con…
jasdbrown May 5, 2020
1f1a087
Fixed issue with merging parameter overrides into Dynamo config
jasdbrown May 5, 2020
0bfd32a
Fixed issue with sending empty strings in config to dynamo
jasdbrown May 6, 2020
52b680d
Fixed error checking, region is not hard-coded and updated table name
jasdbrown May 7, 2020
7299b3b
Updated dynamo table to include timestamp and updated queries to get …
jasdbrown May 8, 2020
8079e62
Fixed bug with config history
jasdbrown May 11, 2020
3734dd8
Config object is now done on a per-stack basis
jasdbrown May 11, 2020
93574e3
Override parameters are now passed through into the stack config
jasdbrown May 11, 2020
ee1aa51
Moved caller, git commit and git origin into config object and top le…
jasdbrown May 11, 2020
c3301d6
Added UsePreviousValue functionality
jasdbrown May 11, 2020
0134244
Added YAML and JSON config export
jasdbrown May 12, 2020
31655fa
Added versions to configs
jasdbrown May 12, 2020
3502a63
Added the ability to list and get versions for a stack config
jasdbrown May 12, 2020
62c2277
Added functionality to rollback to a version with set command for con…
jasdbrown May 12, 2020
7270eec
Fixed issue with merging config and overrides while using previous value
jasdbrown May 13, 2020
5083b2b
Added local secondary index for the state table to make version queri…
jasdbrown May 13, 2020
538092f
Merged stack name and cloudformation stack name
jasdbrown May 13, 2020
6361904
Added JSON parameter option
jasdbrown May 28, 2020
9860d73
Updated README for new features
jasdbrown May 28, 2020
9b970ad
Added state table check
RayWelker May 29, 2020
da6ed02
Merge branch 'feature/dynamo-state' of https://github.com/RightBrain-…
RayWelker May 29, 2020
447880a
removed parameter override when not using parameter Cli
RayWelker Jun 9, 2020
fa0584e
Deployer now returns a non-zero response code on failure
jasdbrown Jun 9, 2020
35c66df
Fixed bug with handling parameters and tests
jasdbrown Jun 9, 2020
124d07c
Fixed issues with automated tests
jasdbrown Jun 10, 2020
7f58eed
Added version rollback test
RayWelker Jun 11, 2020
d4c3d1b
seperated version rollback method into seperate methods
RayWelker Jun 11, 2020
9a1e28f
Updated README and major branches for bumpversion config
jasdbrown Jun 12, 2020
03b443d
Fixed sync test that was failing due to sync timing
jasdbrown Jun 12, 2020
5bc76c4
Setup to run cleanup after tests
yeslayla Jun 13, 2020
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 .bumpversion.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ replace = __version__ = {new_version}

[semver]
main_branches = development
major_branches =
major_branches = release, major
minor_branches = feature
patch_branches = hotfix, bugfix
92 changes: 87 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,25 @@ pip install path/to/deployer-<version>.tar.gz
Deployer is free for use by RightBrain Networks Clients however comes as is with out any guarantees.

##### Flags
* -c --config <config file> (REQUIRED) : Yaml configuration file to run against.
* -c --config <config file> : Yaml configuration file to run against.
* -s --stack <stack name> (REQUIRED) : Stack Name corresponding to a block in the config file.
* -x --execute <execute command> (REQUIRED) : create|update|delete|sync|change Action you wish to take on the stack.
* -p --profile <profile> : AWS CLI Profile to use for AWS commands [CLI Getting Started](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html).
* -P --param <PARAM>, --param PARAM An override for a parameter
* -J --json-param <JSON_PARAM_STRING> :A JSON string for overriding a collection of parameters
* -y --copy <copy> : Copy directory structures specified in the configuration file under the sync_dirs configuration.
* -A --all : Create or Update all stacks in the configuration file, Note you do not have to specify a stack when using this option.
* -r --disable-roleback : Disable rollback on failure, useful when trying to debug a failing stack.
* -r --disable-rollback : Disable rollback on failure, useful when trying to debug a failing stack.
* -t --timeout : Sets Stack create timeout
* -e --events : Display events of the CloudFormation stack at regular intervals.
* -z --zip : Pip install requirements, and zip up lambda's for builds.
* -z --zip-lambas : Pip install requirements, and zip up lambda's for builds.
* -t --change-set-name <change set name> (REQUIRED Change Sets Only) : Used when creating a change set to name the change set.
* -d --change-set-description <change set description> (REQUIRED Change Sets Only) : Used when creating a change set to describe the change set.
* -j, --assume-valid Assumes templates are valid and does not do upstream validation (good for preventing rate limiting)
* -j --assume-valid : Assumes templates are valid and does not do upstream validation (good for preventing rate limiting)
* -O --export-yaml <EXPORT_YAML_FILE> : Export stack config to specified YAML file.
* -o --export-json <EXPORT_JSON_FILE> : Export stack config to specified JSON file.
* -i --config-version <CONFIG_VERSION_ACTION> : Execute ( list | get | set ) of stack config.
* -n --config-version-number <CONFIG_VERSION_NUMBER> : Specified config version, used with --config-version option.
* -D, --debug Sets logging level to DEBUG & enables traceback
* -v, --version Print version number
* --init [INIT] Initialize a skeleton directory
Expand Down Expand Up @@ -58,6 +63,7 @@ Zip up lambdas, copy to s3, and update.
*Note* See [example_configs/dev-us-east-1.yml](./example_configs/dev-us-east-1.yml) for an example configuration file.

The config is a large dictionary. First keys within the dictionary are Stack Names. The global Environment Parameters is a common ground to deduplicate parameter entries that are used in each Stack. Stack parameters overwrite global parameters.
When deployer is run, it creates a DynamoDB Table called CloudFormation-Deployer if it does not already exist. The stack configuration in the config file is saved into DynamoDB, and any future changes result in a new entry with an updated timestamp.

## Required
The following are required for each stack, they can be specified specifically to the stack or in the global config.
Expand Down Expand Up @@ -122,6 +128,32 @@ These parameters provide identity to the Services like what AMI to use or what b
UploadInstanceType: t2.medium
```

Parameters can be overridden from the command line in several different ways.

The first (which takes precedence) is the -P option. Parameters can be specified in the following form:
```
deployer -P 'Param1=Value1'
```
deployer will set the value of parameter 'Param1' to 'Value1', even if it is also specified in the config file. -P can be specified multiple times for multiple parameters

The second is the -J option. Parameters can be specified in the following form:
```
deployer -J '{"Param1":"Value1"}'
```
This option allows the user to specify multiple parameter values as a single JSON object. This will override parameters of the same name as those specified in the config file as well.

It is important to note that since a stack's configuration is saved in the DynamoDB state table, specifying these overrides without sending a config file will use the existing configuration for the stack retrieved from the table, but with the overridden parameter values swapped in.
If it is desirable to send a config file to update some of the parameter values but keep some of the existing values from the previous configuration, it can be done like this:
```
parameters:
Monitoring: 'True'
NginxAMI:
UsePreviousValue: True
NginxInstanceType: t2.medium
```
Notice that for the NginxAMI parameter, the value is now a dictionary instead of a string, and the UsePreviousValue key is set to True. This indicates to deployer to use the existing value in the configuration for the NginxAMI parameter.


## Lookup Parameters

These are parameters that can be pulled from another stack's output. `deployer` tolerates but logs parameters that exist within the configuration but do not exist within the template.
Expand Down Expand Up @@ -172,6 +204,30 @@ Denote that at tranform is used in a stack and deployer will automatically creat
transforms: true
```

## Versions
There are several command line options that allow the user to view and set the configuration based on version number.

When a new configuration is saved automatically to the DynamoDB table, a version number is generated and assigned to it. These versions can be viewed like this:
```
./deployer -s <Stack Name> --config-version list
```
This will output a list of config version numbers and creation timestamps. Viewing a specific configuration based on the number can be done like this:
```
./deployer -s MyStack --config-version get --config-version-number 1
```
In the above example, the output will return the configuration for stack MyStack with the version number 1, the original configuration for the stack. We can then effectively roll back to that configuration with this command:
```
./deployer -s MyStack --config-version set --config-version-number 1
```
This will set the configuration for MyStack back to version 1, reverting the values for parameters, tags, etc.

## Exports
The configuration for a stack can be exported to a file as well. Two formats are supported, JSON and YAML. An example for each is shown here:
```
./deployer -s MyStack --export-yaml ../mystack-config.yaml
./deployer -s MyStack --export-json configs/mystack-config.json
```

## Updates
When running updates to a stack you'll be running updates to the CloudFormation Stack specified by Stack.

Expand Down Expand Up @@ -225,7 +281,7 @@ Currenly there is only the Stack class, Network and Environment classes are now
This is the class that builds zip archives for lambdas and copies directories to s3 given the configuration in the config file.

**Note**
Network Class has been removed, it's irrelivant now. It was in place because of a work around in cloudformation limitations. The abstract class may not be relivant, all of the methods are simmular enough but starting this way provides flexablility if the need arise to model the class in a different way.
Network Class has been removed, it's irrelevant now. It was in place because of a work around in cloudformation limitations. The abstract class may not be relivant, all of the methods are simmular enough but starting this way provides flexablility if the need arise to model the class in a different way.
Comment on lines 283 to +284
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets remove this. Anyone that even recalls that is probably long gone.



# Config Updater
Expand Down Expand Up @@ -353,3 +409,29 @@ Our top template contains numerous references to child templates. Using a combin
```

You can add your own templates under the `cloudformation` directory to deploy your own stacks. Each stack will also need an entry in your deployer config file to specify which directories should be uploaded, the name of the stack, and any required parameters.

# Upgrade path to 1.0.0

A breaking change is made in the 1.0.0 release. The stack_name attribute in the stack configuration is now deprecated. The resulting CloudFormation stack that is created is now the name of the stack definition. For example, consider the following stack definition:

```
deployer:
stack_name: shared-deployer
template: cloudformation/deployer/top.yaml
parameters:
Environment: Something
```

In previous versions, the CloudFormation stack that gets deployed from this is called `shared-deployer`. In 1.0.0+, the CloudFormation stack that gets deployed is called `deployer`.

This means that for existing configurations, the top level stack definition name must be changed to match the stack_name attribute, like this:

```
shared-deployer:
stack_name: shared-deployer
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe comment on this line like:

stack_name: shared-deployer # No longer required

template: cloudformation/deployer/top.yaml
parameters:
Environment: Something
```

This will ensure that deployer recognizes the existing CloudFormation stack, rather than forcing you to create a new one.
120 changes: 103 additions & 17 deletions deployer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python
import argparse
import json
import yaml
import os
from botocore.exceptions import ClientError
from deployer.stack import Stack
Expand All @@ -24,10 +25,11 @@
def main():
# Build arguement parser
parser = argparse.ArgumentParser(description='Deploy CloudFormation Templates')
parser.add_argument("-c", "--config", help="Path to config file.")
parser.add_argument("-c", "--config", help="Path to config file.",default=None)
parser.add_argument("-s", "--stack", help="Stack Name.")
parser.add_argument("-x", "--execute", help="Execute ( create | update | delete | upsert | sync | change ) of stack.")
parser.add_argument("-P", "--param", action='append', help='An override for a parameter')
parser.add_argument("-J", "--json-param", help='A JSON string for overriding a collection of parameters')
parser.add_argument("-p", "--profile", help="Profile.",default=None)
parser.add_argument("-t", "--change-set-name", help="Change Set Name.")
parser.add_argument("-d", "--change-set-description", help="Change Set Description.")
Expand All @@ -40,6 +42,10 @@ def main():
parser.add_argument("-D", "--debug", help="Sets logging level to DEBUG & enables traceback", action="store_true", dest="debug", default=False)
parser.add_argument("-v", "--version", help='Print version number', action='store_true', dest='version')
parser.add_argument("-T", "--timeout", type=int, help='Stack create timeout')
parser.add_argument("-O", "--export-yaml", help="Export stack config to specified YAML file.",default=None)
parser.add_argument("-o", "--export-json", help="Export stack config to specified JSON file.",default=None)
parser.add_argument("-i", "--config-version", help="Execute ( list | get | set ) of stack config.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like config-version isn't super clear that it is referring to a config action.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had previously agreed with you until I saw it used in the read me.
We could leave it as is, move these to -x, or make a new CLI utility.

I think moving to -x probably makes the most sense.

deployer -s some-stack-name -x config-list

@jasdbrown do you have any input on that? Do these flags cause a completely different operation? If I use -i with -x update what happens?

parser.add_argument("-n", "--config-version-number", help="Specified config version, used with --config-version option.")
parser.add_argument('--init', default=None, const='.', nargs='?', help='Initialize a skeleton directory')
parser.add_argument("--disable-color", help='Disables color output', action='store_true', dest='no_color')

Expand Down Expand Up @@ -71,10 +77,19 @@ def main():
# Validate arguements and parameters
options_broken = False
params = {}
if not args.config:
args.config = 'config.yml'
if args.all:
if not args.config:
print(colors['warning'] + "Must Specify config flag!" + colors['reset'])
options_broken = True
if not args.all:
if not args.execute:
if args.config_version:
if args.config_version != "list" and args.config_version != "set" and args.config_version != "get":
Copy link
Contributor

@yeslayla yeslayla May 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that if not args.config_version in ["list", "set", "get"]: might be more clear code. I'm guessing it's not as performant though.

print(colors['warning'] + "config-version command '" + args.config_version + "' not recognized. Must be one of: list, set, get "+ colors['reset'])
options_broken = True
if (args.config_version == 'set' or args.config_version == 'get') and not args.config_version_number:
print(colors['warning'] + "config-version " + args.config_version + " requires config-version-number flag!" + colors['reset'])
options_broken = True
elif not args.execute:
print(colors['warning'] + "Must Specify execute flag!" + colors['reset'])
options_broken = True
if not args.stack:
Expand All @@ -89,6 +104,20 @@ def main():
print(colors['warning'] + "Invalid format for parameter '{}'".format(param) + colors['reset'])
options_broken = True

try:
json_param_dict = {}
if args.json_param:
json_param_dict = json.loads(args.json_param)
if args.param:
#Merge the dicts
merged_params = {**json_param_dict, **params}
params = merged_params
else:
params = json_param_dict
except:
print(colors['warning'] + "Invalid format for json-param, must be valid json." + colors['reset'])
options_broken = True

# Print help output
if options_broken:
parser.print_help()
Expand All @@ -100,33 +129,90 @@ def main():
console_logger.setLevel(logging.ERROR)

try:
# Read Environment Config
with open(args.config) as f:
config = ruamel.yaml.safe_load(f)

# Load stacks into queue
stackQueue = []
if not args.all:
stackQueue = [args.stack]
else:
for stack in config.items():
#Load config, get stacks
try:
with open(args.config) as f:
file_data = ruamel.yaml.safe_load(f)
except Exception as e:
msg = str(e)
logger.error("Failed to retrieve data from config file {}: {}".format(file_name,msg))
exit(3)

for stack in file_data.keys():
if stack[0] != "global":
stackQueue = find_deploy_path(config, stack[0], stackQueue)
stackQueue = find_deploy_path(config_object.get_config(), stack[0], stackQueue)

# Create or update all Environments
for stack in stackQueue:
if stack != 'global' and (args.all or stack == args.stack):

logger.info("Running " + colors['underline'] + str(args.execute) + colors['reset'] + " on stack: " + colors['stack'] + stack + colors['reset'])


# Create deployer config object
cargs = {
'profile': args.profile,
'stack_name': stack
}
if args.config:
cargs['file_name'] = args.config

if args.param or args.json_param:
cargs['override_params'] = params

config_object = Config(**cargs)

#Config Version Handling
if args.config_version:
if args.config_version == "list":
versions = config_object.list_versions()
for version in versions:
if 'version' in version:
print("Timestamp: {} Version: {}".format(version['timestamp'], version['version']))
elif args.config_version == "get":
retrieved_config = config_object.get_version(args.config_version_number)
print(yaml.dump(retrieved_config,default_flow_style=False, allow_unicode=True))
elif args.config_version == "set":
config_object.set_version(args.config_version_number)

continue

#Export if specified
if args.export_json:
config_dict = config_object.get_config()

try:
with open(args.export_json, 'w') as f:
j = json.dumps(config_dict, indent=4)
f.write(j)
except Exception as e:
msg = str(e)
logger.error("Failed to export data to JSON file {}: {}".format(args.export_json,msg))
exit(3)

if args.export_yaml:
config_dict = config_object.get_config()

try:
with open(args.export_yaml, 'w') as f:
yaml.dump(config_dict, f, default_flow_style=False, allow_unicode=True)
except Exception as e:
msg = str(e)
logger.error("Failed to export data to YAML file {}: {}".format(args.export_yaml,msg))
exit(3)

# Build lambdas on `-z`
if args.zip_lambdas:
logger.info("Building lambdas for stack: " + stack)
LambdaPrep(args.config, args.stack).zip_lambdas()

# Create deployer config object
config_object = Config(args.config, stack)

lambda_dirs = config_object.get_config_att('lambda_dirs', [])
sync_base = config_object.get_config_att('sync_base', '.')
LambdaPrep(sync_base, lambda_dirs).zip_lambdas()

# AWS Session object
session = Session(profile_name=args.profile, region_name=config_object.get_config_att('region'))

Expand All @@ -141,15 +227,14 @@ def main():

# S3 bucket to sync to
bucket = CloudtoolsBucket(session, config_object.get_config_att('sync_dest_bucket', None))

# Check whether stack is a stack set or not and assign corresponding object
if(len(config_object.get_config_att('regions', [])) > 0 or len(config_object.get_config_att('accounts', [])) > 0):
env_stack = StackSet(session, stack, config_object, bucket, arguements)
else:
if args.timeout and args.execute not in ['create', 'upsert']:
logger.warning("Timeout specified but action is not 'create'. Timeout will be ignored.")
env_stack = Stack(session, stack, config_object, bucket, arguements)

try:

# Sync files to S3
Expand Down Expand Up @@ -185,6 +270,7 @@ def main():
if args.debug:
tb = sys.exc_info()[2]
traceback.print_tb(tb)
exit(1)

def find_deploy_path(stackConfig, checkStack, resolved = []):
#Generate depedency graph
Expand Down
15 changes: 0 additions & 15 deletions deployer/cloudformation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python
import git
dejonghe marked this conversation as resolved.
Show resolved Hide resolved
from abc import ABCMeta, abstractmethod
from deployer.logger import logger

Expand Down Expand Up @@ -52,20 +51,6 @@ def reload_stack_status(self):
def status(self):
pass

def get_repository(self, base):
try:
return git.Repo(base, search_parent_directories=True)
except git.exc.InvalidGitRepositoryError:
return None

def get_repository_origin(self, repository):
try:
origin = repository.remotes.origin.url
return origin.split('@', 1)[-1] if origin else None
except (StopIteration, ValueError):
return None
return None

def get_template_body(self, bucket, template):
if not bucket:
try:
Expand Down
Loading