Skip to content

Commit 3a960a8

Browse files
committed
Adding code for aws-parameter-syncer
1 parent 067e0a0 commit 3a960a8

File tree

5 files changed

+322
-0
lines changed

5 files changed

+322
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea/*

Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM jfloff/alpine-python:2.7-slim
2+
3+
MAINTAINER Signiant DevOps <devops@signiant.com>
4+
5+
ADD parameter_sync.py /parameter_sync.py
6+
ADD parameter_sync.sh /parameter_sync.sh
7+
8+
RUN pip install boto3
9+
RUN chmod a+x /parameter_sync.py /parameter_sync.sh
10+
11+
ENTRYPOINT ["/parameter_sync.sh"]

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# aws-parameter-syncer
2+
Keep file contents in sync with matching parameters in AWS Parameter Store
3+
4+
## Variables
5+
6+
- VERBOSE - enable more logging if set to 1
7+
- FREQUENCY - How often to check for changes (in seconds). Default is 300 seconds (5 minutes).
8+
- AWS_ACCESS_KEY_ID - The AWS Access Key Id value
9+
- AWS_SECRET_ACCESS_KEY - The AWS Secret Access Key value
10+
- AWS_REGION - AWS Region to search (defaults to us-east-1)
11+
- PARAM_PREFIX - The prefix for the parameters to keep in sync
12+
- resulting filenames will be the parameter name minus the PARAM_PREFIX
13+
- eg.
14+
Following parameters in parameter store: TESTING_param1.txt, TESTING_param2.conf
15+
export PARAM_PREFIX=TESTING_
16+
TESTING_param1.txt will be compared against param1.txt
17+
TESTING_param2.conf will be compared against param2.conf
18+
- CRED_FOLDER_PATH - path to where files are stored (defaults to /credentials)
19+
- in order to access the files outside of the container, make sure to mount this path into the container
20+
21+
22+
## Example Docker runs
23+
24+
25+
This example checks AWS Parameter Store in the default us-east-1 region every 600 seconds (10 minutes)
26+
for parameters containing 'TESTING_'. The credentials in AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are
27+
used to access the AWS Parameter Store. The parameter values will be checked against files in the local
28+
folder 'credentials-dir' which is mounted into the container at '/credentials'.
29+
30+
31+
````
32+
docker run -d -e "FREQUENCY=600" \
33+
-e "VERBOSE=1" \
34+
-e "AWS_ACCESS_KEY_ID=MY_ACCESS_KEY_ID \
35+
-e "AWS_SECRET_ACCESS_KEY=MY_SECRET_KEY \
36+
-e "PARAM_PREFIX=TESTING_" \
37+
-v credentials-dir:/credentials \
38+
signiant/aws-parameter-syncer
39+
````
40+
41+
This example checks AWS Parameter Store in the us-west-2 region every 120 seconds (2 minutes)
42+
for parameters containing 'TESTING_'. The credentials in AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are
43+
used to access the AWS Parameter Store. The parameter values will be checked against files in the local
44+
folder '/my/local/credentials/path' which is mounted into the container at '/some/other/path'.
45+
46+
47+
````
48+
docker run -d -e "FREQUENCY=120" \
49+
-e "AWS_ACCESS_KEY_ID=MY_ACCESS_KEY_ID \
50+
-e "AWS_SECRET_ACCESS_KEY=MY_SECRET_KEY \
51+
-e "AWS_REGION=us-west-2"
52+
-e "PARAM_PREFIX=TESTING_" \
53+
-e "CRED_FOLDER_PATH=/some/other/path"
54+
-v /my/local/credentials/path:/some/other/path \
55+
signiant/aws-parameter-syncer
56+
````
57+
58+
59+
60+
61+

parameter_sync.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import logging, logging.handlers
2+
import argparse
3+
import os
4+
import boto3
5+
import hashlib
6+
import tempfile
7+
import shutil
8+
9+
logging.getLogger("botocore").setLevel(logging.CRITICAL)
10+
11+
def get_sha256_hash(file_path):
12+
logging.debug('Hashing "%s" using SHA256' % file_path)
13+
BUF_SIZE = 65536 # lets read stuff in 64kb chunks!
14+
sha256 = hashlib.sha256()
15+
with open(file_path, 'rb') as f:
16+
while True:
17+
data = f.read(BUF_SIZE)
18+
if not data:
19+
break
20+
sha256.update(data)
21+
22+
logging.debug(' SHA256 hash: {0}'.format(sha256.hexdigest()))
23+
return sha256.digest()
24+
25+
26+
def process_parameters_with_prefix(param_prefix, cred_path, aws_region, aws_access_key=None, aws_secret_key=None, dryrun=False):
27+
logging.debug('Searching for parameters with a prefix of %s' % param_prefix)
28+
29+
def get_parameters(parameter_names_list):
30+
parameter_list = []
31+
if parameter_names_list:
32+
result = ssm.get_parameters(Names=parameter_names_list, WithDecryption=True)
33+
if result:
34+
if 'ResponseMetadata' in result:
35+
if 'HTTPStatusCode' in result['ResponseMetadata']:
36+
if result['ResponseMetadata']['HTTPStatusCode'] == 200:
37+
if 'Parameters' in result:
38+
parameter_list = result['Parameters']
39+
return parameter_list
40+
41+
def process_parameter(param_name, param_value):
42+
filename = param_name.split(param_prefix)[1]
43+
full_cred_path = cred_path + os.sep + filename
44+
existing_file_sha256_hash = None
45+
if os.path.exists(full_cred_path):
46+
existing_file_sha256_hash = get_sha256_hash(full_cred_path)
47+
new_file_full_path = temp_dir + os.sep + filename + '.new'
48+
logging.debug('Storing retrieved value for parameter "%s" in "%s"' % (param_name, new_file_full_path))
49+
with open(new_file_full_path, 'w') as f:
50+
f.write(param_value.replace('\\n', '\n'))
51+
new_file_sha256_hash = get_sha256_hash(new_file_full_path)
52+
logging.debug('Comparing file hashes')
53+
if existing_file_sha256_hash != new_file_sha256_hash:
54+
if not existing_file_sha256_hash:
55+
logging.info('This is a new credentials file: "%s"' % filename)
56+
else:
57+
logging.info("Contents don't match - replacing existing file contents with value from parameter store")
58+
if not dryrun:
59+
if os.path.exists(new_file_full_path) and os.stat(new_file_full_path).st_size > 0:
60+
shutil.copyfile(new_file_full_path, full_cred_path)
61+
else:
62+
logging.error('file %s is missing or zero length - NOT replacing' % new_file_full_path)
63+
else:
64+
logging.info('*** Dryrun selected - will NOT update "%s"' % full_cred_path)
65+
else:
66+
logging.info('Contents of existing "%s" MATCH with value for "%s" from parameter store' % (full_cred_path, param_name))
67+
68+
# Cleanup
69+
if new_file_full_path:
70+
logging.debug('Removing %s' % new_file_full_path)
71+
os.remove(new_file_full_path)
72+
73+
def get_parameters_with_prefix(prefix):
74+
parameter_names_list = []
75+
result = ssm.describe_parameters(Filters=[{'Key': 'Name', 'Values': [param_prefix]}], MaxResults=50)
76+
if result:
77+
if 'ResponseMetadata' in result:
78+
if 'HTTPStatusCode' in result['ResponseMetadata']:
79+
if result['ResponseMetadata']['HTTPStatusCode'] == 200:
80+
if 'Parameters' in result:
81+
for param in result['Parameters']:
82+
logging.debug('Found parameter "%s" - adding to list' % param['Name'])
83+
parameter_names_list.append(param['Name'])
84+
next_token = None
85+
if 'NextToken' in result:
86+
next_token = result['NextToken']
87+
while next_token:
88+
result = ssm.describe_parameters(Filters=[{'Key': 'Name', 'Values': [param_prefix]}], MaxResults=50)
89+
if result:
90+
if 'ResponseMetadata' in result:
91+
if 'HTTPStatusCode' in result['ResponseMetadata']:
92+
if result['ResponseMetadata']['HTTPStatusCode'] == 200:
93+
if 'Parameters' in result:
94+
for param in result['Parameters']:
95+
logging.debug('Found parameter "%s" - adding to list' % param['Name'])
96+
parameter_names_list.append(param['Name'])
97+
next_token = None
98+
if 'NextToken' in result:
99+
next_token = result['NextToken']
100+
return parameter_names_list
101+
102+
# If aws_access_key and aws_secret_key provided, use those
103+
if aws_access_key and aws_secret_key:
104+
session = boto3.session.Session(aws_access_key_id=aws_access_key,
105+
aws_secret_access_key=aws_secret_key,
106+
region_name=aws_region)
107+
else:
108+
session = boto3.session.Session(region_name=aws_region)
109+
110+
ssm = session.client('ssm')
111+
112+
parameter_names_list = get_parameters_with_prefix(param_prefix)
113+
114+
if parameter_names_list:
115+
# Make sure we have a temp dir to work with
116+
temp_dir = tempfile.gettempdir()
117+
if not os.path.exists(temp_dir):
118+
os.makedirs(temp_dir)
119+
120+
for param in get_parameters(parameter_names_list):
121+
parameter_name = param['Name']
122+
parameter_value = param['Value']
123+
process_parameter(parameter_name, parameter_value)
124+
125+
126+
if __name__ == "__main__":
127+
128+
LOG_FILENAME = 'parameter-sync.log'
129+
130+
description = "Script to get all parameters from AWS Parameter\n"
131+
description += "Store with a prefix that matches the given prefix\n\n"
132+
description += "Note: The following environment variables can be set prior to execution\n"
133+
description += " of the script (or alternatively, set them using script parameters)\n\n"
134+
description += " AWS_ACCESS_KEY_ID\n"
135+
description += " AWS_SECRET_ACCESS_KEY"
136+
137+
parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawTextHelpFormatter)
138+
139+
parser.add_argument("--aws-access-key-id", help="AWS Access Key ID", dest='aws_access_key', required=False)
140+
parser.add_argument("--aws-secret-access-key", help="AWS Secret Access Key", dest='aws_secret_key', required=False)
141+
parser.add_argument("--aws-region", help="AWS Region", dest='aws_region', required=True)
142+
parser.add_argument("--param-prefix", help="Parameter prefix", dest='param_prefix', required=True)
143+
parser.add_argument("--credentials-path", help="Where credentials are stored", dest='cred_path', default='/credentials/')
144+
parser.add_argument("--verbose", help="Turn on DEBUG logging", action='store_true', required=False)
145+
parser.add_argument("--dryrun", help="Do a dryrun - no changes will be performed", dest='dryrun',
146+
action='store_true', default=False,
147+
required=False)
148+
args = parser.parse_args()
149+
150+
log_level = logging.INFO
151+
152+
if args.verbose:
153+
print('Verbose logging selected')
154+
log_level = logging.DEBUG
155+
156+
logger = logging.getLogger()
157+
logger.setLevel(logging.DEBUG)
158+
# # create file handler which logs even debug messages
159+
# fh = logging.handlers.RotatingFileHandler(LOG_FILENAME, maxBytes=5242880, backupCount=5)
160+
# fh.setLevel(logging.DEBUG)
161+
# create console handler using level set in log_level
162+
ch = logging.StreamHandler()
163+
ch.setLevel(log_level)
164+
console_formatter = logging.Formatter('%(levelname)8s: %(message)s')
165+
ch.setFormatter(console_formatter)
166+
# file_formatter = logging.Formatter('%(asctime)s - %(levelname)8s: %(message)s')
167+
# fh.setFormatter(file_formatter)
168+
# Add the handlers to the logger
169+
# logger.addHandler(fh)
170+
logger.addHandler(ch)
171+
172+
if not os.environ.get('AWS_ACCESS_KEY_ID') and not args.aws_access_key:
173+
logging.critical('AWS Access Key Id not set - cannot continue')
174+
175+
if not os.environ.get('AWS_SECRET_ACCESS_KEY') and not args.aws_secret_key:
176+
logging.critical('AWS Secret Access Key not set - cannot continue')
177+
178+
logging.debug('INIT')
179+
logging.info('Getting parameters with prefix %s from AWS Parameter Store' % args.param_prefix)
180+
logging.info('Parameter values will be compared against file contents in "%s" and updated if necessary' % args.cred_path)
181+
process_parameters_with_prefix(args.param_prefix, args.cred_path, args.aws_region,
182+
args.aws_access_key, args.aws_secret_key, args.dryrun)
183+
logging.info('COMPLETE')

parameter_sync.sh

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/bin/bash
2+
3+
if [ "$VERBOSE" ]; then
4+
echo "Verbose logging enabled"
5+
set -x
6+
VERBOSE='--verbose'
7+
fi
8+
9+
# Check for required ENV Variables
10+
if [ -z "$PARAM_PREFIX" ]; then
11+
echo "Must supply a parameter prefix by setting the PARAM_PREFIX environment variable"
12+
exit 1
13+
else
14+
echo "Parameter Prefix set to $PARAM_PREFIX"
15+
fi
16+
17+
if [ -z "$AWS_ACCESS_KEY_ID" ]; then
18+
echo "Must supply an AWS Access Key ID by setting the AWS_ACCESS_KEY_ID environment variable"
19+
exit 1
20+
fi
21+
22+
if [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
23+
echo "Must supply an AWS Secret Access Key by setting the AWS_SECRET_ACCESS_KEY environment variable"
24+
exit 1
25+
fi
26+
27+
if [ -z "$AWS_REGION" ]; then
28+
echo "AWS_REGION not set - defaulting to us-east-1"
29+
AWS_REGION='us-east-1'
30+
fi
31+
32+
# Set a default frequency of 300 seconds if not set in the env
33+
if [ -z "$FREQUENCY" ]; then
34+
echo "FREQUENCY not set - defaulting to 300 seconds"
35+
FREQUENCY=300
36+
else
37+
echo "Frequency set to $FREQUENCY seconds"
38+
fi
39+
40+
if [ -z "$CRED_FOLDER_PATH" ]; then
41+
CRED_FOLDER_PATH=/credentials
42+
echo "CRED_FOLDER_PATH environment variable missing - assuming default of ${CRED_FOLDER_PATH}"
43+
fi
44+
45+
if [ ! -e "$CRED_FOLDER_PATH" ]; then
46+
echo "${CRED_FOLDER_PATH} doesn't exist, will attempt to create it"
47+
echo "NOTE: If this was not expected - please mount credentials path at ${CRED_FOLDER_PATH}"
48+
mkdir -p ${CRED_FOLDER_PATH}
49+
if [ "$?" -ne 0 ]; then
50+
echo "Unable to create ${CRED_FOLDER_PATH} - exiting..."
51+
exit 1
52+
fi
53+
fi
54+
55+
# Loop forever, sleeping for our frequency
56+
while true
57+
do
58+
echo "Awoke to check for new credentials with prefix ${PARAM_PREFIX} in AWS Parameter Store"
59+
60+
python /parameter_sync.py --credentials-path ${CRED_FOLDER_PATH} --param-prefix ${PARAM_PREFIX} --aws-region ${AWS_REGION} ${VERBOSE}
61+
echo "Sleeping for $FREQUENCY seconds"
62+
sleep $FREQUENCY
63+
echo
64+
done
65+
66+
exit 0

0 commit comments

Comments
 (0)