1+ #!/usr/bin/python2
2+
13import boto3
24import json
35import os
79zone_id = os .environ ['ZONE_ID' ]
810service = os .environ ['SERVICE' ]
911ttl = int (os .environ ['TTL' ])
10- dns_role_arn = os .environ .get ('DNS_ROLE_ARN' )
1112
1213private_instance_record_template = os .environ ['PRIVATE_INSTANCE_RECORD_TEMPLATE' ]
1314private_asg_record_template = os .environ ['PRIVATE_ASG_RECORD_TEMPLATE' ]
1819manage_public_asg_dns = os .environ ['MANAGE_PUBLIC_ASG_DNS' ].lower () in ["true" , "1" ]
1920
2021aws_region = os .environ .get ('AWS_DEFAULT_REGION' )
21-
22+ r53_client = boto3 . client ( 'route53' )
2223ec2_resource = boto3 .resource ('ec2' , region_name = aws_region )
2324asg_client = boto3 .client ('autoscaling' , region_name = aws_region )
2425
25-
26- def get_session (role_arn , sts_client ):
27- """
28- Assumes a role in the given account and returns a Boto3 session.
29-
30- """
31-
32- # Assume the role and get back credentials.
33- acc_id = sts_client .get_caller_identity ()['Account' ]
34- response = sts_client .assume_role (
35- RoleArn = role_arn ,
36- RoleSessionName = '{}-updates-in-{}-from-{}' .format (service , zone_id , acc_id ),
37- DurationSeconds = 900 ,
38- )
39- creds = response ['Credentials' ]
40-
41- # Return a new session using those credentials.
42- session = boto3 .Session (
43- aws_access_key_id = creds ['AccessKeyId' ],
44- aws_secret_access_key = creds ['SecretAccessKey' ],
45- aws_session_token = creds ['SessionToken' ],
46- )
47- return session
26+ # get domain name
27+ try :
28+ hostedzone = r53_client .get_hosted_zone (Id = zone_id )
29+ domain = hostedzone ['HostedZone' ]['Name' ]
30+ except Exception as e :
31+ print e
32+ raise
4833
4934
5035def generate_record_name (template , ** kwargs ):
@@ -73,136 +58,48 @@ def generate_record_name(template, **kwargs):
7358 return names_map [template ].substitute (** kwargs )
7459
7560
76- def change_rrs (changes , zoneid , r53_client ):
77- """
78- make DNS update
79- :param changes: list of R53 changes
80- :param zoneid: string
81- :return: response dict
82- """
83- print ('Performing change {}' .format (json .dumps (changes )))
84- response = r53_client .change_resource_record_sets (
85- HostedZoneId = '/hostedzone/{}' .format (zoneid ),
86- ChangeBatch = {
87- 'Comment' : 'Updated by Lambda Function' ,
88- 'Changes' : changes
89- }
90- )
91- return response
92-
93-
94- def parse_event (event ):
95- """
96- :param event: event object received by lambda
97- :return: tuple containing message and metadata from an event object
98- """
99- metadata = {}
100- message = json .loads (event ['Records' ][0 ]['Sns' ]['Message' ])
101- if 'NotificationMetadata' in list (message .keys ()):
102- metadata = json .loads (message ['NotificationMetadata' ])
103- return message , metadata
104-
105-
106- def get_instance_metadata (instance_id ):
107- """
108- :param instance_id: string
109- :return: dict containing info about instance
110- """
111-
112- instance = ec2_resource .Instance (instance_id )
113- # we only want running instances
114- if instance .state ['Name' ] not in ['running' ]:
115- return False
116- metadata = {
117- 'private_ip' : instance .private_ip_address ,
118- 'private_hostname' : instance .private_dns_name ,
119- 'public_ip' : instance .public_ip_address ,
120- 'az' : instance .placement ['AvailabilityZone' ]
121- }
122- return metadata
123-
124-
125- def get_asg_instances (asg_name , asg_event , message ):
126- """
127- :param asg_name: string
128- :return: dict of dicts with keys being instance IDs and values
129- being instance information as returned by get_instance_metadata
130- """
131- asg = asg_client .describe_auto_scaling_groups (
132- AutoScalingGroupNames = [asg_name ])
133- # get instances IDs
134- asg_instances = [i ['InstanceId' ]
135- for i in asg ['AutoScalingGroups' ][0 ]['Instances' ]]
136-
137- # ensure launching instance is found in the asg
138- ec2_instance = ""
139- if asg_event == "autoscaling:EC2_INSTANCE_LAUNCH" :
140- ec2_instance = message ['EC2InstanceId' ]
141- if ec2_instance not in asg_instances :
142- raise Exception ('Launched instance not found in asg' )
143-
144- return_value = {}
145- for instance in asg_instances :
146- metadata = get_instance_metadata (instance )
147- if metadata :
148- return_value [instance ] = metadata
149- else :
150- # ensure metadata is found for launching instance
151- if instance == ec2_instance and asg_event == "autoscaling:EC2_INSTANCE_LAUNCH" :
152- raise Exception ('No metadata returned for ' + instance )
153- return return_value
154-
155-
15661def lambda_handler (event , context ):
15762
158- # assume role in DNS account if dns_role_arn is specified
159- if dns_role_arn :
160- sts_client = boto3 .client ('sts' )
161- dns_session = get_session (dns_role_arn , sts_client )
162- r53_client = dns_session .client ('route53' )
163- else :
164- r53_client = boto3 .client ('route53' )
165-
166- # get domain name
167- hostedzone = r53_client .get_hosted_zone (Id = zone_id )
168- domain = hostedzone ['HostedZone' ]['Name' ]
169-
170- # parse event
17163 message , metadata = parse_event (event )
17264
173- print ( 'Received event: {}' .format (json .dumps (event ) ))
174- print ( 'Message: {}' .format (json .dumps (message ) ))
175- print ( 'Metadata: {}' .format (json .dumps (metadata ) ))
65+ print 'Received event: {}' .format (json .dumps (event ))
66+ print 'Message: {}' .format (json .dumps (message ))
67+ print 'Metadata: {}' .format (json .dumps (metadata ))
17668
17769 # asg name and event
17870 asg_name = message ['AutoScalingGroupName' ]
17971 asg_event = message ['Event' ]
18072
18173 # get metadata of all instances in ASG
18274 instances_metadata = get_asg_instances (asg_name , asg_event , message )
183- print ( 'ASG INSTANCES: {}' .format (json .dumps (instances_metadata ) ))
75+ print 'ASG INSTANCES: {}' .format (json .dumps (instances_metadata ))
18476
18577 # create a list of public addresses of all instances in ASG
18678 asg_public_ips = []
187- for _metadata in instances_metadata .values ():
79+ for _metadata in instances_metadata .itervalues ():
18880 if _metadata ['public_ip' ] is not None :
18981 asg_public_ips .append (_metadata ['public_ip' ])
19082 else :
19183 continue
192- print ( 'ASG_PUBLIC_IPS: {}' .format (json .dumps (asg_public_ips ) ))
84+ print 'ASG_PUBLIC_IPS: {}' .format (json .dumps (asg_public_ips ))
19385
19486 # create a list of private addresses of asg instances
19587 asg_private_ips = [instances_metadata [i ]['private_ip' ]
196- for i in instances_metadata .keys ()]
197- print ( 'ASG_PRIVATE_IPS: {}' .format (json .dumps (asg_private_ips ) ))
88+ for i in instances_metadata .iterkeys ()]
89+ print 'ASG_PRIVATE_IPS: {}' .format (json .dumps (asg_private_ips ))
19890
19991 # holds DNS changes to do
20092 changes = []
93+ asg_event_types = {
94+ 'launch' : 'autoscaling:EC2_INSTANCE_LAUNCH' ,
95+ 'terminate' : 'autoscaling:EC2_INSTANCE_TERMINATE' ,
96+ 'test' : 'autoscaling:TEST_NOTIFICATION' ,
97+ }
20198
20299 if manage_instance_dns :
203100 # instance has been launched or asg created
204101 if instances_metadata :
205- for instance_id , instance_info in instances_metadata .items ():
102+ for instance_id , instance_info in instances_metadata .iteritems ():
206103 instance_ip = instance_info ['private_ip' ]
207104 az = instance_info ['az' ]
208105 az_short = az .split ('-' )[- 1 ]
@@ -268,4 +165,111 @@ def lambda_handler(event, context):
268165
269166 # apply dns updates
270167 if changes :
271- change_rrs (changes , zone_id , r53_client )
168+ change_rrs (changes , zone_id )
169+
170+
171+ # helpers
172+
173+ def find_record (name , zone_id ):
174+ """
175+ find resource record
176+ :param name: string, record name to find
177+ :param zone_id: string
178+ :return: resource record:
179+ [{u'Name': 'myrecord.kops.askredhat.com.',
180+ u'ResourceRecords': [{u'Value': '10.10.10.10'},
181+ {u'Value': '10.10.10.11'},
182+ {u'Value': '10.10.10.12'}],
183+ u'TTL': 300,
184+ u'Type': 'A'}]
185+ """
186+ paginator = r53_client .get_paginator ('list_resource_record_sets' )
187+ page_iterator = paginator .paginate (
188+ HostedZoneId = 'hostedzone/{}' .format (zone_id ))
189+ rrs = None
190+ for page in page_iterator :
191+ rrs = filter (lambda record :
192+ record ['Name' ] == name , page ['ResourceRecordSets' ])
193+ if rrs :
194+ break
195+ return rrs
196+
197+
198+ def change_rrs (changes , zoneid ):
199+ """
200+ make DNS update
201+ :param changes: list of R53 changes
202+ :param zoneid: string
203+ :return: response dict
204+ """
205+ print 'Performing change {}' .format (json .dumps (changes ))
206+ response = r53_client .change_resource_record_sets (
207+ HostedZoneId = '/hostedzone/{}' .format (zoneid ),
208+ ChangeBatch = {
209+ 'Comment' : 'Updated by Lambda Function' ,
210+ 'Changes' : changes
211+ }
212+ )
213+ return response
214+
215+
216+ def parse_event (event ):
217+ """
218+ :param event: event object received by lambda
219+ :return: tuple containing message and metadata from an event object
220+ """
221+ metadata = {}
222+ message = json .loads (event ['Records' ][0 ]['Sns' ]['Message' ])
223+ if 'NotificationMetadata' in message .keys ():
224+ metadata = json .loads (message ['NotificationMetadata' ])
225+ return message , metadata
226+
227+
228+ def get_instance_metadata (instance_id ):
229+ """
230+ :param instance_id: string
231+ :return: dict containing info about instance
232+ """
233+
234+ instance = ec2_resource .Instance (instance_id )
235+ # we only want running instances
236+ if instance .state ['Name' ] not in ['running' ]:
237+ return False
238+ metadata = {
239+ 'private_ip' : instance .private_ip_address ,
240+ 'private_hostname' : instance .private_dns_name ,
241+ 'public_ip' : instance .public_ip_address ,
242+ 'az' : instance .placement ['AvailabilityZone' ]
243+ }
244+ return metadata
245+
246+
247+ def get_asg_instances (asg_name , asg_event , message ):
248+ """
249+ :param asg_name: string
250+ :return: dict of dicts with keys being instance IDs and values
251+ being instance information as returned by get_instance_metadata
252+ """
253+ asg = asg_client .describe_auto_scaling_groups (
254+ AutoScalingGroupNames = [asg_name ])
255+ # get instances IDs
256+ asg_instances = [i ['InstanceId' ]
257+ for i in asg ['AutoScalingGroups' ][0 ]['Instances' ]]
258+
259+ # ensure launching instance is found in the asg
260+ ec2_instance = ""
261+ if asg_event == "autoscaling:EC2_INSTANCE_LAUNCH" :
262+ ec2_instance = message ['EC2InstanceId' ]
263+ if ec2_instance not in asg_instances :
264+ raise Exception ('Launched instance not found in asg' )
265+
266+ return_value = {}
267+ for instance in asg_instances :
268+ metadata = get_instance_metadata (instance )
269+ if metadata :
270+ return_value [instance ] = metadata
271+ else :
272+ # ensure metadata is found for launching instance
273+ if instance == ec2_instance and asg_event == "autoscaling:EC2_INSTANCE_LAUNCH" :
274+ raise Exception ('No metadata returned for ' + instance )
275+ return return_value
0 commit comments