diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..68cc742 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "aws-lambda-ddns-function"] + path = aws-lambda-ddns-function + url = https://github.com/aws-samples/aws-lambda-ddns-function diff --git a/availabilty_zones.tf b/.off/availabilty_zones.tf similarity index 100% rename from availabilty_zones.tf rename to .off/availabilty_zones.tf diff --git a/variables.common.availability_zones.tf b/.off/variables.common.availability_zones.tf similarity index 100% rename from variables.common.availability_zones.tf rename to .off/variables.common.availability_zones.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 93cda0b..4e10255 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,12 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.48.0 + rev: v1.62.3 hooks: # - id: terraform_validate - id: terraform_fmt - - id: terraform_docs_replace - args: ['table'] + - id: terraform_docs + args: + - --args=--config=.terraform-docs.yml exclude: common/*.tf exclude: version.tf exclude: examples/ @@ -13,7 +14,7 @@ repos: args: [ "--args=--config=__GIT_WORKING_DIR__/.tflint.hcl"] exclude: examples/ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.1.0 hooks: - id: check-symlinks - id: detect-aws-credentials diff --git a/.terraform-docs.yml b/.terraform-docs.yml new file mode 100644 index 0000000..418f24a --- /dev/null +++ b/.terraform-docs.yml @@ -0,0 +1,45 @@ +formatter: markdown table + +header-from: main.tf +footer-from: "" + +sections: +## hide: [] + show: + - data-sources + - header + - footer + - inputs + - modules + - outputs + - providers + - requirements + - resources + +output: + file: README.md + mode: replace +# mode: inject +# template: |- +# +# {{ .Content }} +# + +## output-values: +## enabled: false +## from: "" +## +## sort: +## enabled: true +## by: name +## +## settings: +## anchor: true +## color: true +## default: true +## description: false +## escape: true +## indent: 2 +## required: true +## sensitive: true +## type: true diff --git a/.tflint.hcl b/.tflint.hcl index fcc2fa8..09d6863 100644 --- a/.tflint.hcl +++ b/.tflint.hcl @@ -19,3 +19,9 @@ rule "aws_instance_invalid_type" { plugin "aws" { enabled = true } + +# https://github.com/terraform-linters/tflint/blob/v0.33.1/docs/rules/terraform_module_pinned_source.md +rule "terraform_module_pinned_source" { + enabled = false +} + diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce3418..257bcda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Versions -* v1.0.0 -- {{ yyyy-mm-dd }} +* v1.0.0 -- 2022-01-21 - initial creation diff --git a/README.md b/README.md index 659a771..7b3f2af 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,56 @@ -# aws-dynamic-route53 - -## About - -This module will construct all the resources to allow for automated C2 DNS registration in Route53. This is largely sourced from the -AWS blog on [DNS in a Multiaccount Environment with Route53](https://aws.amazon.com/blogs/security/simplify-dns-management-in-a-multiaccount-environment-with-route-53-resolver/) . We have added to it to also do PTR registration, as well as -making it IPv6 ready. - -The [code](https://github.com/aws-samples/aws-lambda-ddns-function) from that blog is linked in as a submodule under [aws-lambda-ddns-function](aws-lambda-ddns-function/). - -This code is intended to be deployed per region, and will handle all of the DNS registration for EC2 -instances deployed, assumign specific tags exist. - -It will create: - -- DynamoDB Table (inf-dynamic-route53-{region}) -- IAM Roles -- Lambda -- CloudWatch Events -- CloudWatch Log - -## Operation - -See the the [blog](#blog) for full details on how it works. The short version is: - -- Cloudwatch event on instance (starting, started, terminated) -- Run lambda -- On startup - - Get instance details (id, region, ipv4, ipv6) - - Determine zone from tag(s) - - Find zone - - Add records if found - - Log action - - Record in DDB name and details -- On terminate - - Get instance detail (id) - - search DDB table for id - - Remove records, if in table - - Log action - - -## Tags - -A number of tags will be used to affect behavior of the DNS entries. - -### Tag: Name - -The `Name` tag is the primary tag that will be used to determine the DNS name to create. It is expected to be a unique FQDN. If no Name -tag is provided, the hostname portion of the name will be constructed from the IP address: - -* IPv4 - * ip address: A.B.C.D - * hostname: ip-A-B-C-D -* IPv6 (TBD) - -The domain portion of the `Name` tag must exist within Route53 in order for any records to be created. - -### Tag: boc:dns:zone - -The `boc:dns:zone` tag will be used in case we need to force a specific domain name on a host, either because it cannot obtain -the proper zone (domain) from the `Name` tag, of that a custom per-instance `Name` tag cannot be created. This latter condition -occurs for systems which work from a launch template, such as EMR or EKS. - -### Tag: boc:dns:alias - -The `boc:dns:alias` tag is used to create an alternate DNS name (CNAME), pointed to the primary name. It is an FQDN, and the same conditions -apply as with [Name](#tag--name). - -# Links - -## github aws-lambda-ddns-function - * https://github.com/aws-samples/aws-lambda-ddns-function -## Blog - * https://aws.amazon.com/blogs/security/simplify-dns-management-in-a-multiaccount-environment-with-route-53-resolver/ - - -# Repository Setup Details - -* One time - -```script -git submodule add https://github.com/aws-samples/aws-lambda-ddns-function aws-lambda-ddns-function -git commit -m'add submodule' aws-lambda-ddns-function -``` - -* After first clone - -```script -git submodule update --init -``` - -* Pull new stuff from submoduule - -```script -git submodule foreach git pull origin master -``` + +## Requirements + +| Name | Version | +|------|---------| +| [aws](#requirement\_aws) | >= 3.66.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 3.66.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_event_rule.ec2_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | +| [aws_cloudwatch_event_target.ec2_target](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | +| [aws_cloudwatch_log_group.log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_dynamodb_table.table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | +| [aws_iam_role.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_alias.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_alias) | resource | +| [aws_lambda_function.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_permission.allow_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [aws_arn.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy.lambda_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | +| [aws_iam_policy_document.lambda_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.lambda_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_alias](#input\_account\_alias) | AWS Account Alias | `string` | `""` | no | +| [account\_id](#input\_account\_id) | AWS Account ID (default will pull from current user) | `string` | `""` | no | +| [component\_tags](#input\_component\_tags) | Additional tags for Components (s3, kms, ddb) | `map(map(string))` |
{
"ddb": {},
"kms": {},
"s3": {}
}
| no | +| [create](#input\_create) | Flag to indicate whether to create the resources or not (default: true) | `bool` | `true` | no | +| [dynamodb\_table\_name](#input\_dynamodb\_table\_name) | Different DynamoDB table name to override default of var.name) | `string` | `null` | no | +| [lambda\_environment\_variables](#input\_lambda\_environment\_variables) | Map of lambda environment variables and values | `map(string)` |
{
"DNS_RR_TimeToLive": 60,
"DynamoDBName": null,
"SleepTime": 60,
"TagKeyCname": "boc:dns:cname",
"TagKeyHostName": "TBD",
"TagKeyZone": "boc:dns:zone"
}
| no | +| [lambda\_name](#input\_lambda\_name) | Different Lambda name to override default of var.name) | `string` | `null` | no | +| [name](#input\_name) | Name to use within all the created resources (default: inf-dynamic-route53) | `string` | `"inf-dynamic-route53"` | no | +| [override\_prefixes](#input\_override\_prefixes) | Override built-in prefixes by component. This should be used primarily for common infrastructure things | `map(string)` | `{}` | no | +| [tags](#input\_tags) | AWS Tags to apply to appropriate resources | `map(string)` | `{}` | no | + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/cloudwatch.tf b/cloudwatch.tf new file mode 100644 index 0000000..c1c9613 --- /dev/null +++ b/cloudwatch.tf @@ -0,0 +1,63 @@ +locals { + cloudwatch_name = format("/aws/lambda/%v", local.lambda_name) + cloudwatch_event_pattern = { + "source" = ["aws.ec2"] + "detail-type" = ["EC2 Instance State-change Notification"] + "detail" = { + "state" = ["running", "shutting-down", "stopped"] + } + } +} + +resource "aws_cloudwatch_log_group" "log" { + count = var.create ? 1 : 0 + name = local.cloudwatch_name + # kms_key_id = var.kms_key_arn + retention_in_days = lookup(local._defaults["cloudwatch"], "retention_in_days", 7) + + tags = merge( + local.base_tags, + var.tags, + map("Name", local.name), + ) +} + +# aws events put-targets --rule ec2_lambda_ddns_rule --targets Id=id123456789012,Arn= + +resource "aws_cloudwatch_event_rule" "ec2_rule" { + count = var.create ? 1 : 0 + name = local.name + description = "Capture EC2 Events to hande dynamic Route53 registration" + event_pattern = jsonencode(local.cloudwatch_event_pattern) + + tags = merge( + local.base_tags, + var.tags, + map("Name", local.name), + ) +} + +resource "aws_cloudwatch_event_target" "ec2_target" { + count = var.create ? 1 : 0 + # target_id = local.name + target_id = var.create ? aws_lambda_function.lambda[0].function_name : null + arn = var.create ? aws_lambda_function.lambda[0].arn : null + rule = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].name : null + # propagate_tags = true + + # tags = merge( + # local.base_tags, + # var.tags, + # map("Name", local.name), + # ) +} + +resource "aws_lambda_permission" "allow_cloudwatch" { + count = var.create ? 1 : 0 + statement_id = "AllowExecutionFromCloudWatch" + action = "lambda:InvokeFunction" + function_name = var.create ? aws_lambda_function.lambda[0].function_name : null + principal = "events.amazonaws.com" + source_arn = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].arn : null + # qualifier = var.create ? aws_lambda_alias.lambda[0].name : null +} diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py new file mode 100755 index 0000000..6b23b48 --- /dev/null +++ b/code/ddns-lambda.py @@ -0,0 +1,1675 @@ +# To do +# 1. read custom host name field to use that as DNS over IP address + +""" +DDNS Lambda Python3 Script + +This script will perform the following functions. + +if no CNAME or ZONE tags is set on the ec2 instance, and not using a custom dhcp option set: +1. Script will do nothing + +if no CNAME or ZONE tags are set, but are using a custom dhcp option set with +a hosted zone created, which matches the domain name. +1. An 'A' record is created to the IP +2. A 'PTR" record is create to the DNS name + +if a CNAME tag is set. +1. Creates a CNAME to the DNS name +2. Creates a PTR record to the CNAME + +if a ZONE tag is set. +1. Creates an 'A' record to the IP +2. Creates a 'PTR" record to the DNS name +""" +import json +import sys +import datetime +import random +import logging +import re +import uuid +import time +import inspect +import boto3 +import os +from botocore.exceptions import ClientError + +# Setting Global Variables +LOGGER = logging.getLogger() +ACCOUNT = None +REGION = None +VERSION = '0.0.5' + +# Adjust the logging level [logging.INFO, logging.DEBUG, logging.WARNING, etc] +LOGGER.setLevel(logging.DEBUG) +# SNS_CLIENT = None + +# Read Env variables +SLEEPTIME = int(os.environ.get('SleepTime','60')) +DDBNAME = os.environ.get('DynamoDBName','inf-dynamic-route53') +TAGKEY_CNAME = os.environ.get('TagKeyCname','boc:dns:cname') +TAGKEY_ZONE = os.environ.get('TagKeyZone','boc:dns:zone') +TAGKEY_HOSTNAME = os.environ.get('TagKeyHostName','') +DNS_RR_TTL = int(os.environ.get('DNS_RR_TimeToLive','60')) +DNS_RR_TTL = 60 if DNS_RR_TTL==0 else DNS_RR_TTL + +print('Loading function v%s: %s'.format(VERSION,datetime.datetime.now().time().isoformat())) + +def lineno(): # pragma: no cover + """ + Returns the current line number in our script + :return: + """ + return str(' - line number: ' + str(inspect.currentframe().f_back.f_lineno)) + +# def get_sns_client(): +# """ +# Get sns client +# :return: +# """ +# try: +# return boto3.client('sns') +# except ClientError as err: +# print("Unexpected error: %s" % err) + +def get_route53_client(): + """ + Get route53 client + :return: + """ + try: + return boto3.client('route53') + except ClientError as err: + print("Unexpected error: %s" % err) + +def get_ec2_client(): + """ + Get ec2 client + :return: + """ + try: + return boto3.client('ec2') + except ClientError as err: + print("Unexpected error: %s" % err) + +def get_dynamodb_client(): + """ + Get dynamodb client + :return: + """ + try: + return boto3.client('dynamodb') + except ClientError as err: + print("Unexpected error: %s" % err) + +def lambda_handler( + event, + context, + dynamodb_client=get_dynamodb_client(), + compute=get_ec2_client(), + route53=get_route53_client() +): + """ + Check to see whether a DynamoDB table already exists. If not, create it. + This table is used to keep a record of instances that have been created + along with their attributes. This is necessary because when you terminate an instance + its attributes are no longer available, so they have to be fetched from the table. + :param event: + :param context: + :param dynamodb_client: + :param compute: + :param route53: + :param sns_client: + :return: + """ + LOGGER.info("event: %s", str(event) + lineno()) + LOGGER.info("context: %s", str(context) + lineno()) + # SNS_CLIENT = sns_client + + caller_response = [] + # Checking to make sure there is a dynamodb table named in the Env Variable + tables = list_tables(dynamodb_client) + + LOGGER.info("tables: %s", str(tables)) + if DDBNAME in tables['TableNames']: + LOGGER.info('DynamoDB table already exists') + else: + LOGGER.info('DynamoDB table does not exist, exiting function: %s', DDBNAME) + return None + # commented out by awspeter + # create_table(dynamodb_client, DDBNAME) + + # Set variables + # Get the state from the Event stream + state = event['detail']['state'] + LOGGER.debug("instance state: %s", str(state) + lineno()) + + # Get the instance id, region, and tag collection + instance_id = event['detail']['instance-id'] + LOGGER.debug("instance id: %s", str(instance_id) + lineno()) + #ACCOUNT = event['account'] + region = event['region'] + #REGION = region + LOGGER.debug("region: %s", str(region) + lineno()) + + # Only doing something if the state is running + if state == 'running': + LOGGER.debug("sleeping for {} seconds {}".format(SLEEPTIME,lineno())) + + if "pytest" in sys.modules: + # called from within a test run + time.sleep(1) + else: + # called "normally" + time.sleep(SLEEPTIME) + + # Get instance information + instance = get_instances(compute, instance_id) + # Remove response metadata from the response + if 'ResponseMetadata' in instance: + instance.pop('ResponseMetadata') + # Remove null values from the response. You cannot save a dict/JSON + # document in DynamoDB if it contains null values + LOGGER.debug("instance: %s", str(instance) + lineno()) + instance = remove_empty_from_dict(instance) + instance_dump = json.dumps(instance, default=json_serial) + instance_attributes = json.loads(instance_dump) + LOGGER.debug("instance_attributes: %s", str(instance_attributes) + lineno()) + LOGGER.debug("trying to put instance information in " + "dynamo table %s", str(instance_attributes) + lineno()) + put_item_in_dynamodb_table(dynamodb_client, DDBNAME, instance_id, instance_attributes) + LOGGER.debug("done putting item in dynamo table %s", lineno()) + else: + # Fetch item from DynamoDB + LOGGER.debug("Fetching instance information from dynamodb %s", lineno()) + instance = get_item_from_dynamodb_table(dynamodb_client, DDBNAME, instance_id) + LOGGER.debug("instance: %s", str(instance) + lineno()) + + # Get the instance tags and reorder them because we want a zone created before CNAME + try: + tags = instance['Reservations'][0]['Instances'][0]['Tags'] + except: + tags = [] + + LOGGER.debug("tags are: %s", str(tags) + lineno()) + + # tag_type = determine_tag_type(tags) + tag_type = determine_tag_type(tags)[0] # changed to return a list, so read the 1st value + has_custom_hostname = determine_tag_type(tags)[1] # if hostname is found in the + + LOGGER.debug("tag type: %s", str(tag_type) + lineno()) + LOGGER.debug("has custom hostname tag: %s", str(has_custom_hostname) + lineno()) + + if tag_type == 'invalid': + LOGGER.info("Must have either CNAME or ZONE in tags, can not have both tags" + lineno()) + exit(-1) + + LOGGER.debug("Get instance attributes %s", lineno()) + LOGGER.debug("instance: %s", str(instance) + lineno()) + LOGGER.debug("type: %s", str(type(instance)) + lineno()) + if instance and 'Reservations' in instance: + LOGGER.debug("reservations: %s", str(instance['Reservations']) + lineno()) + LOGGER.debug("reservations: %s", str(instance['Reservations'][0]) + lineno()) + LOGGER.debug("reservations: %s", str(instance['Reservations'][0]['Instances']) + lineno()) + LOGGER.debug("reservations:" + " %s", str(instance['Reservations'][0]['Instances'][0]) + lineno()) + + private_ip = instance['Reservations'][0]['Instances'][0]['PrivateIpAddress'] + private_dns_name = instance['Reservations'][0]['Instances'][0]['PrivateDnsName'] + private_host_name = private_dns_name.split('.')[0] + + LOGGER.debug("private ip: %s", str(private_ip) + lineno()) + LOGGER.debug("private_dns_name: %s", str(private_dns_name) + lineno()) + LOGGER.debug("private_host_name: %s", str(private_host_name) + lineno()) + + # awspeter - commneted out public dns + # public_ip = None + # public_dns_name = None + + # awspeter - commneted out public dns + # if 'PublicIpAddress' in instance['Reservations'][0]['Instances'][0]: + # LOGGER.debug('instance has public ip address key') + # try: + # LOGGER.debug("instance: %s", str(instance) + lineno()) + # if 'Reservations' in instance: + # LOGGER.debug("reservations: %s", str(instance['Reservations'][0])) + # if 'Instances' in instance['Reservations'][0]: + # LOGGER.debug("instances: %s", str(instance['Reservations'][0]['Instances'][0])) + # public_ip = instance['Reservations'][0]['Instances'][0]['PublicIpAddress'] + # LOGGER.debug("public_ip: %s", str(public_ip) + lineno()) + # if public_ip and 'PublicDnsName' not in instance['Reservations'][0]['Instances'][0]: + # LOGGER.info("Could not find PublicDnsName for public instance, check that vpc has dns hostnames enabled:" + lineno()) + # exit() + # else: + # public_dns_name = instance['Reservations'][0]['Instances'][0]['PublicDnsName'] + # LOGGER.debug("public_dns_name: %s", str(public_dns_name) + lineno()) + # public_host_name = public_dns_name.split('.')[0] + # LOGGER.debug("public_host_name: %s", str(public_host_name)) + # except BaseException as err: + # LOGGER.info("Unexpected error: %s", str(err)) + + # Get the subnet mask of the instance + subnet_id = instance['Reservations'][0]['Instances'][0]['SubnetId'] + LOGGER.debug("subnet_id: %s", str(subnet_id) + lineno()) + cidr_block = get_subnet_cidr_block(compute, subnet_id) + LOGGER.debug("cidr_block: %s", str(cidr_block) + lineno()) + subnet_mask = int(cidr_block.split('/')[-1]) + LOGGER.debug("subnet_mask: %s", str(subnet_mask) + lineno()) + reversed_ip_address = reverse_list(private_ip) + + reversed_domain_prefix = get_reversed_domain_prefix(subnet_mask, private_ip) + reversed_domain_prefix = reverse_list(reversed_domain_prefix) + LOGGER.debug("reversed_domain_prefix is: %s", str(reversed_domain_prefix) + lineno()) + # Set the reverse lookup zone + reversed_lookup_zone = reversed_domain_prefix + 'in-addr.arpa.' + LOGGER.info("The reverse lookup zone for this instance is: %s", str(reversed_lookup_zone)) + + # Get VPC id + vpc_id = instance['Reservations'][0]['Instances'][0]['VpcId'] + + # Are DNS Hostnames and DNS Support enabled? + if is_dns_hostnames_enabled(compute, vpc_id): + LOGGER.debug("DNS hostnames enabled for %s", str(vpc_id) + lineno()) + else: + LOGGER.debug("DNS hostnames disabled for %s. You have to enable DNS hostnames to use Route 53 private hosted zones. %s", vpc_id, lineno()) + if is_dns_support_enabled(compute, vpc_id): + LOGGER.debug("DNS support enabled for %s", str(vpc_id) + lineno()) + else: + LOGGER.debug("DNS support disabled for %s. You have to enabled DNS support to use Route 53 private hosted zones. %s", str(vpc_id), lineno()) + exit() + + # Create the public and private hosted zone collections. + # These are collections of zones in Route 53. + hosted_zones = list_hosted_zones(route53) + LOGGER.debug("hosted_zones: %s", str(hosted_zones) + lineno()) + private_hosted_zones = get_private_hosted_zones(hosted_zones) + LOGGER.debug("private_hosted_zones: %s", str(list(private_hosted_zones)) + lineno()) + private_hosted_zone_collection = get_private_hosted_zone_collection(private_hosted_zones) + LOGGER.debug("private_hosted_zone_collection: %s", + str(list(private_hosted_zone_collection)) + lineno()) + + # awspeter - commneted out public dns + # public_hosted_zones = get_public_hosted_zones(hosted_zones) + # LOGGER.debug("public_hosted_zones: %s", str(list(public_hosted_zones)) + lineno()) + # public_hosted_zones_collection = get_public_hosted_zone_collection(public_hosted_zones) + # LOGGER.debug("public_hosted_zones_collection:" + # " %s", str(list(public_hosted_zones_collection)) + lineno()) + + # Check to see whether a reverse lookup zone for the instance + # already exists. If it does, check to see whether + # the reverse lookup zone is associated with the instance's + # VPC. If it isn't create the association. You don't + # need to do this when you create the reverse lookup + # zone because the association is done automatically. + LOGGER.info("reversed_lookup_zone: %s", str(reversed_lookup_zone) + lineno()) + reverse_zone = None + for record in hosted_zones['HostedZones']: + LOGGER.debug("record name: %s", str(record['Name']) + lineno()) + if record['Name'] == reversed_lookup_zone: + reverse_zone = record['Name'] + break + if reverse_zone: + LOGGER.debug("Reverse lookup zone found: %s", str(reversed_lookup_zone) + lineno()) + reverse_lookup_zone_id = get_zone_id(route53, reversed_lookup_zone) + LOGGER.debug("reverse_lookup_zone_id: %s", str(reverse_lookup_zone_id) + lineno()) + + reverse_hosted_zone_properties = get_hosted_zone_properties(route53, reverse_lookup_zone_id) + LOGGER.debug("reverse_hosted_zone_properties:" + " %s", str(reverse_hosted_zone_properties) + lineno()) + + if vpc_id in map(lambda x: x['VPCId'], reverse_hosted_zone_properties['VPCs']): + LOGGER.info("Reverse lookup zone %s is associated with VPC %s %s", reverse_lookup_zone_id, vpc_id, lineno()) + reverse_zone_associated = True + else: + LOGGER.info("Reverse lookup zone %s is NOT associated with VPC %s %s", reverse_lookup_zone_id, vpc_id, lineno()) + reverse_zone_associated = False + + # awspeter - commmeted out vpc association + # LOGGER.info("Associating zone %s with VPC %s", reverse_lookup_zone_id, vpc_id) + # try: + # associate_zone(route53, reverse_lookup_zone_id, region, vpc_id) + # except BaseException as err: + # LOGGER.debug("%s", str(err)+lineno()) + else: + LOGGER.info("No matching reverse lookup zone, PTR record will not be created %s", lineno()) + # LOGGER.info("No matching reverse lookup zone, so we will create one %s", lineno()) + # # create private hosted zone for reverse lookups + # if state == 'running': + # create_reverse_lookup_zone(route53, instance, reversed_domain_prefix, region) + # reverse_lookup_zone_id = get_zone_id(route53, reversed_lookup_zone) + + # Wait a random amount of time. This is a poor-mans back-off + # if a lot of instances are launched all at once. + time.sleep(random.random()) + + if tag_type == 'cname_selected': + # We must have a cname because we want reverse dns to point to the A record + cname = get_cname_from_tags(tags) + cname_prefix = cname.split('.')[0] + # if not cname: + # publish_to_sns( + # SNS_CLIENT, + # ACCOUNT, REGION, + # "Must have a CNAME tag for lambda to work. " + # "Please add CNAME to instance tags" + lineno() + # ) + + LOGGER.debug("iterating through tags %s", lineno()) + # Loop through the instance's tags, looking for the zone and + # cname tags. If either of these tags exist, check + # to make sure that the name is valid. If it is and + # if there's a matching zone in DNS, create A and PTR records. + for tag in tags: + LOGGER.debug("#### tag: %s", str(tag) + lineno()) + if TAGKEY_ZONE in tag.get('Key', {}).lstrip().upper(): + + # Simple check to make sure the hostname is valid + if is_valid_hostname(tag.get('Value')): + LOGGER.debug("hostname is valid %s", lineno()) + LOGGER.debug("checking if value in private:" + " %s", str(list(private_hosted_zone_collection)) + lineno()) + # awspeter - commneted out public dns + # LOGGER.debug("checking if value in public:" + # " %s", str(list(public_hosted_zones_collection)) + lineno()) + + if tag.get('Value').lstrip().lower() in private_hosted_zone_collection: + LOGGER.debug("Private zone found: %s", str(tag.get('Value')) + lineno()) + private_hosted_zone_name = tag.get('Value').lstrip().lower() + LOGGER.debug("private_zone_name: %s", str(private_hosted_zone_name) + lineno()) + private_hosted_zone_id = get_zone_id(route53, private_hosted_zone_name) + LOGGER.debug("private_hosted_zone_id:" + " %s", str(private_hosted_zone_id) + lineno()) + private_hosted_zone_properties = get_hosted_zone_properties(route53, private_hosted_zone_id) + LOGGER.debug("private_hosted_zone_properties:" + " %s", str(private_hosted_zone_properties) + lineno()) + fqdn = private_host_name + '.' + private_hosted_zone_name + if state == 'running': + found_vpc_id = False + if 'VPCs' in private_hosted_zone_properties: + for vpc in private_hosted_zone_properties['VPCs']: + if vpc['VPCId'] == vpc_id: + found_vpc_id = True + if found_vpc_id: + LOGGER.info("Private hosted zone %s is associated with VPC %s %s", private_hosted_zone_id, vpc_id, lineno()) + else: + LOGGER.info("Private hosted zone %s is NOT associated with VPC %s %s", private_hosted_zone_id, vpc_id, lineno()) + # LOGGER.info("Associating zone %s with VPC %s %s", private_hosted_zone_id, vpc_id, lineno()) + # try: + # associate_zone(route53, private_hosted_zone_id, region, vpc_id) + # except BaseException as err: + # LOGGER.info('You cannot create an association with a VPC with an overlapping subdomain.\n', err) + # exit() + try: + if found_vpc_id: + create_resource_record( + route53, + private_hosted_zone_id, + private_host_name, + private_hosted_zone_name, + 'A', + private_ip + ) + + LOGGER.debug("appending to caller response %s", lineno()) + + caller_response.append('Created A record in zone id: ' + + str(private_hosted_zone_id) + + ' for hosted zone ' + + str(private_host_name) + '.' + + str(private_hosted_zone_name) + + ' with value: ' + + str(private_ip)) + + if reverse_zone_associated: + create_resource_record( + route53, + reverse_lookup_zone_id, + reversed_ip_address, + 'in-addr.arpa', + 'PTR', + fqdn +# private_dns_name + ) + + caller_response.append('Created PTR record in zone id: ' + + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + + 'in-addr.arpa with value: ' + + str(fqdn)) +# str(private_dns_name)) + + except BaseException as err: + LOGGER.debug("%s", str(err)+lineno()) + else: + try: + delete_resource_record( + route53, + private_hosted_zone_id, + private_host_name, + private_hosted_zone_name, + 'A', + private_ip + ) + + caller_response.append('Deleted A record in zone id: ' + + str(private_hosted_zone_id) + + ' for hosted zone ' + + str(private_host_name) + '.' + + str(private_hosted_zone_name) + + ' with value: ' + + str(private_ip)) + + delete_resource_record( + route53, + reverse_lookup_zone_id, + reversed_ip_address, + 'in-addr.arpa', + 'PTR', + fqdn +# private_dns_name + ) + + caller_response.append('Deleted PTR record in zone id: ' + + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + '.' + + str(private_dns_name) + + ' with value: ' + + str(fqdn)) +# str(private_dns_name)) + + except BaseException as err: + LOGGER.debug("%s", str(err)+lineno()) + # create PTR record + # awspeter - commneted out public dns + # elif tag.get('Value').lstrip().lower() in public_hosted_zones_collection: + # LOGGER.debug("Public zone found %s", tag.get('Value') + lineno()) + # public_hosted_zone_name = tag.get('Value').lstrip().lower() + + # public_hosted_zone_id = get_zone_id( + # route53, + # public_hosted_zone_name, + # private_zone=False + # ) + # # create A record in public zone + # if state == 'running': + # try: + # create_resource_record( + # route53, + # public_hosted_zone_id, + # cname_prefix, + # public_hosted_zone_name, + # 'A', + # public_ip + # ) + # caller_response.append('Created A record in zone id: ' + + # str(public_hosted_zone_id) + + # ' for hosted zone ' + + # str(cname_prefix) + '.' + + # str(public_hosted_zone_name) + + # ' with value: ' + + # str(public_ip)) + # except BaseException as err: + # LOGGER.debug("%s", str(err) + lineno()) + # else: + # try: + # delete_resource_record( + # route53, + # public_hosted_zone_id, + # cname_prefix, + # public_hosted_zone_name, + # 'A', + # public_ip + # ) + # caller_response.append('Deleted A record in zone id: ' + + # str(public_hosted_zone_id) + + # ' for hosted zone ' + + # str(cname_prefix) + '.' + + # str(public_hosted_zone_name) + + # ' with value: ' + + # str(public_ip)) + # except BaseException as err: + # LOGGER.debug("%s", str(err) + lineno()) + else: + LOGGER.info("No matching zone found for %s", tag.get('Value')) + else: + LOGGER.info("%s is not a valid host name %s", tag.get('Value'), lineno()) + # Consider making this an elif CNAME + else: + LOGGER.debug("The tag \'%s\' is not a zone tag %s", str(tag.get('Key')), lineno()) + + if TAGKEY_CNAME.upper() in tag.get('Key', {}).lstrip().upper(): + + # Simple hostname check + if is_valid_hostname(tag.get('Value')): + + LOGGER.debug("CNAME hostname of %s is valid %s", str(tag.get('Value')), lineno()) + # convert the cname value to lower case and strip whitespace and newline characters + icname = tag.get('Value').lstrip().lower() + + LOGGER.debug("icname: %s", str(icname) + lineno()) + # Gets the prefix for the cname + cname_host_name = icname.split('.')[0] + LOGGER.debug("cname_host_name: %s", str(cname_host_name) + lineno()) + + # Gets suffix + cname_domain_suffix = icname[icname.find('.') + 1:] + LOGGER.debug("cname_domain_suffix: %s", str(cname_domain_suffix) + lineno()) + + # Try and find the hosted zone with the cname suffix + cname_domain_suffix_id = get_zone_id(route53, cname_domain_suffix) + + LOGGER.debug("cname_domain_suffix_id: %s", str(cname_domain_suffix_id)) + # Iterate of the private hosted zones + LOGGER.debug("Iterating over private hosted zones %s", lineno()) + for cname_private_hosted_zone in private_hosted_zone_collection: + + LOGGER.debug("cname for private hosted zone in private hosted zone collection: %s", str(cname_private_hosted_zone) + lineno()) + + cname_private_hosted_zone_id = get_zone_id(route53, cname_private_hosted_zone) + LOGGER.debug("cname_private_hosted_zone_id:" + " %s", str(cname_private_hosted_zone_id) + lineno()) + LOGGER.debug("cname_domain_suffix_id:" + " %s", str(cname_domain_suffix_id) + lineno()) + + if cname_domain_suffix_id == cname_private_hosted_zone_id: + LOGGER.debug("cname_domain_suffix_id:" + " %s", str(cname_domain_suffix_id) + lineno()) + + if cname.endswith(cname_private_hosted_zone): + LOGGER.debug("cname ends with" + " %s", str(cname_private_hosted_zone) + lineno()) + + # create CNAME record in private zone + if state == 'running': + try: + LOGGER.debug("creating resource record %s", lineno()) + LOGGER.debug("private_dns_name:" + " %s", str(private_dns_name) + lineno()) + create_resource_record( + route53, + cname_private_hosted_zone_id, + cname_host_name, + cname_private_hosted_zone, + 'CNAME', + private_dns_name + ) + + caller_response.append('Created CNAME record in zone id: ' + + str(cname_private_hosted_zone_id) + + ' for hosted zone ' + + str(cname_host_name) + '.' + + str(cname_private_hosted_zone) + + ' with value: ' + + str(private_dns_name)) + + except BaseException as err: + LOGGER.debug("%s", str(err) + lineno()) + else: + try: + LOGGER.debug("deleting resource record %s", lineno()) + delete_resource_record( + route53, + cname_private_hosted_zone_id, + cname_host_name, + cname_private_hosted_zone, + 'CNAME', + private_dns_name + ) + + caller_response.append('Deleted CNAME record in zone id: ' + + str(cname_private_hosted_zone_id) + + ' for hosted zone ' + + str(cname_host_name) + '.' + + str(cname_private_hosted_zone) + + ' with value: ' + + str(private_dns_name)) + + except BaseException as err: + LOGGER.debug("%s", str(err) + lineno()) + # awspeter - commented out public record + # # Only do public if there is public ip on instance + # if public_dns_name: + # # Iterate over the public hosted zones + # LOGGER.debug("Iterating over public hosted zones %s", lineno()) + # for cname_public_hosted_zone in public_hosted_zones_collection: + # LOGGER.debug("cname in public hosted zone:" + # " %s", str(cname_public_hosted_zone) + lineno()) + # LOGGER.debug("cname is: %s", str(cname) + lineno()) + # if cname.endswith(cname_public_hosted_zone): + # cname_public_hosted_zone_id = get_zone_id( + # route53, + # cname_public_hosted_zone, + # False + # ) + # LOGGER.debug("cname_public_hosted_zone_id:" + # " %s", str(cname_public_hosted_zone_id) + lineno()) + + # # create CNAME record in public zone + # if state == 'running': + # try: + # create_resource_record( + # route53, + # cname_public_hosted_zone_id, + # cname_host_name, + # cname_public_hosted_zone, + # 'CNAME', + # public_dns_name + # ) + + # caller_response.append('Created CNAME record in zone id: ' + + # str(cname_public_hosted_zone_id) + + # ' for hosted zone ' + + # str(cname_host_name) + '.' + + # str(cname_public_hosted_zone) + + # ' with value: ' + + # str(public_dns_name)) + + # except BaseException as err: + # LOGGER.debug("%s", str(err) + lineno()) + # else: + # try: + # delete_resource_record( + # route53, + # cname_public_hosted_zone_id, + # cname_host_name, + # cname_public_hosted_zone, + # 'CNAME', + # public_dns_name + # ) + + # caller_response.append('Deleted CNAME record in zone id: ' + + # str(cname_public_hosted_zone_id) + + # ' for hosted zone ' + + # str(cname_host_name) + '.' + + # str(cname_public_hosted_zone) + + # ' with value: ' + + # str(public_dns_name)) + + # except BaseException as err: + # LOGGER.debug("%s", str(err) + lineno()) + + # Is there a DHCP option set? + # Get DHCP option set configuration + LOGGER.debug("\n#############\nIterate over DHCP option sets %s\n", lineno()) + + try: + LOGGER.debug("trying to get dhcp option set id %s", lineno()) + dhcp_options_id = get_dhcp_option_set_id_for_vpc(compute, vpc_id) + LOGGER.debug("dhcp_options_id: %s", str(dhcp_options_id) + lineno()) + dhcp_configurations = get_dhcp_configurations(compute, dhcp_options_id) + LOGGER.debug("dhcp_configurations: %s", str(get_dhcp_configurations) + lineno()) + + except BaseException as err: + LOGGER.info("No DHCP option set assigned to this VPC %s\n", str(err)+lineno()) + exit() + + # Look to see whether there's a DHCP option set assigned to + # the VPC. If there is, use the value of the domain name + # to create resource records in the appropriate Route 53 + # private hosted zone. This will also check to see whether + # there's an association between the instance's VPC and + # the private hosted zone. If there isn't, it will create it. + for configuration in dhcp_configurations: + + LOGGER.debug("configuration: %s", str(configuration) + lineno()) + LOGGER.debug("private hosted zones: %s", str(private_hosted_zone_collection) + lineno()) + + if configuration in private_hosted_zone_collection: + private_hosted_zone_name = configuration + LOGGER.debug("Private zone found %s", str(private_hosted_zone_name) + lineno()) + + private_hosted_zone_id = get_zone_id(route53, private_hosted_zone_name) + LOGGER.debug("Private_hosted_zone_id: %s", str(private_hosted_zone_id) + lineno()) + private_hosted_zone_properties = get_hosted_zone_properties( + route53, + private_hosted_zone_id + ) + + LOGGER.debug("private_hosted_zone_properties:" + " %s", str(private_hosted_zone_properties) + lineno()) + + # create A records and PTR records + fqdn = private_host_name + '.' + private_hosted_zone_name + if state == 'running': + if vpc_id in map(lambda x: x['VPCId'], private_hosted_zone_properties['VPCs']): + vpc_associated = True + LOGGER.info("Private hosted zone %s is associated with VPC %s %s", private_hosted_zone_id, vpc_id, lineno()) + else: + vpc_associated = False + LOGGER.info("Private hosted zone %s is NOT associated with VPC %s %s", private_hosted_zone_id, vpc_id, lineno()) + exit() + # LOGGER.info("Associating zone %s with VPC" + # " %s %s", private_hosted_zone_id, vpc_id, lineno()) + # try: + # associate_zone(route53, private_hosted_zone_id, region, vpc_id) + # except BaseException as err: + # LOGGER.info("You cannot create an association with a VPC with an overlapping subdomain. %s\n", str(err)) + # exit() + try: + + if not has_custom_hostname: + if vpc_associated: + LOGGER.debug("Creating resource records %s", lineno()) + create_resource_record( + route53, + private_hosted_zone_id, + private_host_name, + private_hosted_zone_name, + 'A', + private_ip + ) + + caller_response.append('Created A record in zone id: ' + + str(private_hosted_zone_id) + + ' for hosted zone ' + + str(private_host_name) + '.' + + str(private_hosted_zone_name) + + ' with value: ' + + str(private_ip)) + else: + LOGGER.debug("No forward zone associated with VPC - skipping creating resource records %s", lineno()) + if reverse_zone_associated: + create_resource_record( + route53, + reverse_lookup_zone_id, + reversed_ip_address, + 'in-addr.arpa', + 'PTR', + fqdn +# private_dns_name + ) + + caller_response.append('Created PTR record in zone id: ' + + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + + 'in-addr.arpa with value: ' + + str(fqdn)) +# str(private_dns_name)) + else: + LOGGER.debug("No reverse zone associated with VPC - skipping creating resource records %s", lineno()) + + else: + if vpc_associated: + LOGGER.debug("Creating resource records %s", lineno()) + create_resource_record( + route53, + private_hosted_zone_id, + cname_prefix, # awspeter - that should be private host + private_hosted_zone_name, + 'A', + private_ip + ) + + caller_response.append('Created A record in zone id: ' + + str(private_hosted_zone_id) + ' for hosted zone ' + + str(cname_prefix) + '.' + + str(private_hosted_zone_name) + ' with value: ' + + str(private_ip)) + else: + LOGGER.debug("No forward zone associated with VPC - skipping creating resource records %s", lineno()) + if reverse_zone_associated: + create_resource_record( + route53, + reverse_lookup_zone_id, + reversed_ip_address, + 'in-addr.arpa', + 'PTR', + cname + ) + + caller_response.append('Created PTR record in zone id: ' + + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + + 'in-addr.arpa with value: ' + + str(cname)) + else: + LOGGER.debug("No reverse zone associated with VPC - skipping creating resource records %s", lineno()) + + except BaseException as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + else: + + LOGGER.debug("Deleting resource records: %s", lineno()) + try: + if not has_custom_hostname: + delete_resource_record( + route53, + private_hosted_zone_id, + private_host_name, + private_hosted_zone_name, + 'A', + private_ip + ) + caller_response.append('Deleted A record in zone id: ' + + str(private_hosted_zone_id) + ' for hosted zone ' + + str(private_host_name) + '.' + + str(private_hosted_zone_name) + ' with value: ' + + str(private_ip)) + + delete_resource_record( + route53, + reverse_lookup_zone_id, + reversed_ip_address, + 'in-addr.arpa', + 'PTR', + fqdn +# private_dns_name + ) + caller_response.append('Deleted PTR record in zone id: ' + + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + + 'in-addr.arpa with value: ' + + str(fqdn)) +# str(private_dns_name)) + else: + delete_resource_record( + route53, + private_hosted_zone_id, + cname_prefix, + private_hosted_zone_name, + 'A', + private_ip + ) + caller_response.append('Deleted A record in zone id: ' + + str(private_hosted_zone_id) + ' for hosted zone ' + + str(cname_prefix) + '.' + + str(private_hosted_zone_name) + ' with value: ' + + str(private_ip)) + delete_resource_record( + route53, + reverse_lookup_zone_id, + reversed_ip_address, + 'in-addr.arpa', + 'PTR', + cname + ) + + caller_response.append('Deleted PTR record in zone id: ' + + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + + 'in-addr.arpa with value: ' + + str(cname)) + + except BaseException as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + else: + LOGGER.debug("No matching zone for %s", str(configuration) + lineno()) + + # Clean up DynamoDB after deleting records + if state != 'running': + delete_item_from_dynamodb_table(dynamodb_client, DDBNAME, instance_id) + + caller_response.insert(0, 'Successfully removed recordsets') + return caller_response + + caller_response.insert(0, 'Successfully created recordsets') + + return caller_response + +def determine_tag_type(tags): + """ + Determine tag type - CNAME or ZONE + :param tags: + :return: + """ + cname = -1 + zone = -1 + has_custom_hostname = False + + for item in tags: + LOGGER.debug("item: %s", str(item) + lineno()) + + if item['Key'].lower() == TAGKEY_CNAME.lower(): + cname = 1 + elif item['Key'].lower() == TAGKEY_ZONE.lower(): + zone = 1 + elif item['Key'].lower() == TAGKEY_HOSTNAME.lower(): + has_custom_hostname = True + + if cname < 0 and zone < 0: + return [None, has_custom_hostname] + elif cname > 0 and zone < 0: + return ['cname_selected', has_custom_hostname] + elif cname < 0 and zone > 0: + return ['zone_selected', has_custom_hostname] + return 'invalid' + +def get_cname_from_tags(tags): + """ + Get the cname prefix from tags + :param tags: + :return: + """ + + try: + for tag in tags: + LOGGER.debug("tag: %s", str(tag)) + + if TAGKEY_CNAME.upper() in tag.get('Key', {}).lstrip().upper(): + cname = tag.get('Value').lstrip().lower() + + return cname + return None + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0])) + +def get_instances(client, instance_id): + """ + Get ec2 instance information + :return: + """ + try: + return client.describe_instances(InstanceIds=[instance_id]) + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def list_hosted_zones(client): + """ + Get route53 hosted zones + :param client: + :return: + """ + try: + return client.list_hosted_zones() + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def list_tables(client): + """ + List the dynamodb tables + :param client: + :return: + """ + try: + return client.list_tables() + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def delete_item_from_dynamodb_table(client, table, instance_id): + """ + Delete the item from dynamodb table + :param client: + :param table: + :param instance_id: + :return: + """ + try: + return client.delete_item( + TableName=table, + Key={ + 'InstanceId': {'S': instance_id} + }) + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def put_item_in_dynamodb_table(client, table, instance_id, instance_attributes): + """ + Put item in dynamodb table + :param client: + :param table: + :param instance_id: + :param instance_attributes: + :return: + """ + try: + LOGGER.debug("attributes: %s", str(instance_attributes) + lineno()) + LOGGER.debug("putting attributes: %s", str(instance_attributes) + lineno()) + + return client.put_item( + TableName=str(table), + Item={ + 'InstanceId': {'S': instance_id}, + 'InstanceAttributes': {'S': str(instance_attributes)} + } + ) + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def get_item_from_dynamodb_table(client, table, instance_id): + """ + Get item from dynamodb table + :param client: + :param table: + :param instance_id: + :return: + """ + try: + # Fetch item from DynamoDB + item = client.get_item( + TableName=table, + Key={ + 'InstanceId': { + 'S': instance_id + } + }, + AttributesToGet=[ + 'InstanceAttributes' + ] + ) + + if 'Item' in item: + LOGGER.debug("returned item:" + " %s", str(item['Item']['InstanceAttributes']['S']) + lineno()) + item = item['Item']['InstanceAttributes']['S'].replace("'", '"') + item = item.replace(" True,", ' "True",') + item = item.replace(" False,", ' "False",') + LOGGER.debug("item: %s", str(item) + lineno()) + return json.loads(item) + return None + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def get_private_hosted_zone_collection(private_hosted_zones): + """ + Get private hosted zone collection + :param private_hosted_zones: + :return: + """ + try: + private_hosted_zone_collection = [] + + for item in private_hosted_zones: + LOGGER.debug("item: %s", str(item) + lineno()) + private_hosted_zone_collection.append(item['Name']) + + return private_hosted_zone_collection + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +# awspeter - commneted out public dns +# def get_public_hosted_zone_collection(public_hosted_zones): +# """ +# Get public hosted zone collection +# :param public_hosted_zones: +# :return: +# """ +# try: +# public_hosted_zone_collection = [] + +# for item in public_hosted_zones: +# LOGGER.debug("items: %s", str(item) + lineno()) +# public_hosted_zone_collection.append(item['Name']) + +# return public_hosted_zone_collection +# except: +# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +# awspeter - commneted out public dns +# def get_public_hosted_zones(hosted_zones): +# """ +# Get public hosted zones +# :param hosted_zones: +# :return: +# """ +# try: +# public_hosted_zones = [] + +# for item in hosted_zones['HostedZones']: +# LOGGER.debug("item: %s", str(item) + lineno()) + +# if not item['Config']['PrivateZone']: +# public_hosted_zones.append(item) + +# return public_hosted_zones +# except: +# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def get_private_hosted_zones(hosted_zones): + """ + Get private hosted zones + :param hosted_zones: + :return: + """ + try: + private_hosted_zones = [] + + for item in hosted_zones['HostedZones']: + LOGGER.debug("item: %s", str(item) + lineno()) + + if item['Config']['PrivateZone']: + private_hosted_zones.append(item) + + return private_hosted_zones + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def get_dhcp_option_set_id_for_vpc(client, vpc_id): + """ + Get the dhcp option set from vpc + :param client: + :param vpc_id: + :return: + """ + try: + option_sets = {} + + results = client.describe_vpcs() + + for item in results['Vpcs']: + + if 'DhcpOptionsId' in item: + option_sets[str(item['VpcId'])] = item['DhcpOptionsId'] + else: + option_sets[str(item['VpcId'])] = None + + return option_sets[vpc_id] + + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +# def create_dynamodb_table(client, table_name): +# """ +# Create dynamodb table +# :param client: +# :param table_name: +# :return: +# """ +# try: +# return client.create_table( +# TableName=table_name, +# AttributeDefinitions=[ +# { +# 'AttributeName': 'InstanceId', +# 'AttributeType': 'S' +# }, +# ], +# KeySchema=[ +# { +# 'AttributeName': 'InstanceId', +# 'KeyType': 'HASH' +# }, +# ], +# ProvisionedThroughput={ +# 'ReadCapacityUnits': 4, +# 'WriteCapacityUnits': 4 +# } +# ) +# except ClientError as err: +# LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def get_dynamodb_table(client, table_name): + """ + Get the dynamodb table + :param client: + :param table_name: + :return: + """ + try: + return client.describe_table( + TableName=table_name + ) + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +# def create_table(client, table_name): +# """ +# Create dynamodb table +# :param client: +# :param table_name: +# :return: +# """ +# try: +# create_dynamodb_table(client, table_name) +# created = -1 +# while created < 0: +# table = get_dynamodb_table(client, table_name) + +# if table['Table']['TableStatus'] == 'ACTIVE': +# created = 1 +# else: +# time.sleep(15) + +# return True +# except: +# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def change_resource_recordset(client, zone_id, host_name, hosted_zone_name, record_type, value): + """ + Change resource recordset + :param client: + :param zone_id: + :param host_name: + :param hosted_zone_name: + :param value: + :return: + """ + try: + response = client.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + "Comment": "Updated by Lambda DDNS", + "Changes": [ + { + "Action": "UPSERT", + "ResourceRecordSet": { + "Name": host_name + hosted_zone_name, + "Type": record_type, + "TTL": DNS_RR_TTL, + "ResourceRecords": [ + { + "Value": value + }, + ] + } + }, + ] + } + ) + + LOGGER.debug("response: %s", str(response) + lineno()) + return response + except ClientError as err: + LOGGER.debug("Error creating resource record: %s", str(err) + lineno()) + error_message = str(err) + + if "conflicts with other records" in error_message: + LOGGER.debug("Can not create dns record because of duplicates: %s", str(err) + lineno()) + return 'Duplicate resource record' + elif "conflicting RRSet" in error_message: + LOGGER.debug("Can not create dns record because of duplicates: %s", str(err) + lineno()) + return 'Conflicting resource record' + else: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + return 'Unexpected error: ' + str(err) + +def create_resource_record(client, zone_id, host_name, hosted_zone_name, record_type, value): + """ + This function creates resource records in the hosted zone passed by the calling function. + :param client: + :param zone_id: + :param host_name: + :param hosted_zone_name: + :param record_type: + :param value: + :return: + """ + LOGGER.debug("Creating resource record: zone_id: %s host_name:" + " %s hosted_zone_name: %s record_type: %s value: %s %s", zone_id, + host_name, hosted_zone_name, record_type, value, lineno()) + try: + if host_name[-1] != '.': + host_name = host_name + '.' + + LOGGER.debug( + "Updating %s in zone %s%s to %s %s", record_type, host_name, + hosted_zone_name, value, lineno()) + + # To prevent rate throttling + time.sleep(1) + + response = change_resource_recordset( + client, + zone_id, + host_name, + hosted_zone_name, + record_type, + value + ) + + LOGGER.debug("response: %s", str(response) + lineno()) + return response + except ClientError as err: + LOGGER.debug("Error creating resource record: %s", str(err) + lineno()) + if 'is not permitted as it conflicts with other records ' \ + 'with the same DNS name in zone' in str(err): + LOGGER.debug("Can not create dns record because " + "of duplicates: %s", str(err) + lineno()) + +def delete_resource_record(client, zone_id, host_name, hosted_zone_name, record_type, value): + """ + This function deletes resource records from the hosted zone passed by the calling function. + :param client: + :param zone_id: + :param host_name: + :param hosted_zone_name: + :param record_type: + :param value: + :return: + """ + try: + LOGGER.debug("Deleting %s record %s in zone %s" + " %s", record_type, host_name, hosted_zone_name, lineno()) + if host_name[-1] != '.': + host_name = host_name + '.' + response = client.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + "Comment": "Updated by Lambda DDNS", + "Changes": [ + { + "Action": "DELETE", + "ResourceRecordSet": { + "Name": host_name + hosted_zone_name, + "Type": record_type, + "TTL": DNS_RR_TTL, + "ResourceRecords": [ + { + "Value": value + }, + ] + } + } + ] + } + ) + + LOGGER.debug("delete record response: %s", str(response) + lineno()) + return response + + except ClientError as err: + if 'Not Found' in str(err): + LOGGER.debug("Record not found error: %s", str(err)+lineno()) + return + + if 'InvalidChangeBatch' in str(err) and 'it was not found' in str(err): + LOGGER.debug("Record not found error: %s", str(err)+lineno()) + return + + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def get_zone_id(client, zone_name, private_zone=True): + """ + This function returns the zone id for the zone name that's passed into the function. + :param client: + :param zone_name: + :return: + """ + try: + if zone_name[-1] != '.': + zone_name = zone_name + '.' + hosted_zones = list_hosted_zones(client) + + LOGGER.debug("zone name: %s", str(zone_name) + lineno()) + LOGGER.debug("hosted_zones: %s", str(hosted_zones) + lineno()) + zones = [] + for record in hosted_zones['HostedZones']: + LOGGER.debug("record: %s", str(record) + lineno()) + if record['Config']['PrivateZone'] == private_zone: + if record['Name'] == zone_name: + zones.append(record) + LOGGER.debug("zones: %s", str(zones) + lineno()) + + try: + zone_id_long = zones[0]['Id'] + LOGGER.debug("zone id: %s", str(zone_id_long) + lineno()) + zone_id = str.split(str(zone_id_long), '/')[2] + return zone_id + except: + return None + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def is_valid_hostname(hostname): + """ + This function checks to see whether the hostname entered + into the zone and cname tags is a valid hostname. + :param hostname: + :return: + """ + try: + LOGGER.debug("determining if hostname is valid: %s", str(hostname) + lineno()) + if hostname is None or len(hostname) > 255: + return False + if hostname[-1] == ".": + hostname = hostname[:-1] + allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(? 0: + my_list.append(int(item)) + + LOGGER.debug("list1: %s", str(my_list) + lineno()) + LOGGER.debug("type: %s", str(type(my_list)) + lineno()) + + my_list.reverse() + reversed_list = '' + for item in my_list: + reversed_list = reversed_list + str(item) + '.' + LOGGER.debug("returning: %s", str(reversed_list) + lineno()) + return reversed_list + + LOGGER.info('Not a valid ip') + exit() + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def get_reversed_domain_prefix(subnet_mask, private_ip): + """ + Uses the mask to get the zone prefix for the reverse lookup zone + :param subnet_mask: + :param private_ip: + :return: + """ + try: + LOGGER.debug("### Subnet mask: %s", str(subnet_mask) + lineno()) + LOGGER.debug("### Private ip: %s", str(private_ip) + lineno()) + + third_octet = re.search(r"\d{1,3}.\d{1,3}.\d{1,3}.", private_ip) + return third_octet.group(0) + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +# def create_reverse_lookup_zone(client, instance, reversed_domain_prefix, region): +# """ +# Creates the reverse lookup zone. +# :param client: +# :param instance: +# :param reversed_domain_prefix: +# :param region: +# :return: +# """ +# try: +# LOGGER.debug('Creating reverse lookup zone %s in.addr.arpa.' +# ' %s', str(reversed_domain_prefix), lineno()) + +# if reversed_domain_prefix[-1] == ".": +# reversed_domain_prefix = reversed_domain_prefix[:-1] + +# return client.create_hosted_zone( +# Name=reversed_domain_prefix + '.in-addr.arpa.', +# VPC={ +# 'VPCRegion': region, +# 'VPCId': instance['Reservations'][0]['Instances'][0]['VpcId'] +# }, +# CallerReference=str(uuid.uuid1()), +# HostedZoneConfig={ +# 'Comment': 'Updated by Lambda DDNS' +# } +# ) +# except: +# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def json_serial(obj): + """ + JSON serializer for objects not serializable by default json code + :param obj: + :return: + """ + try: + if isinstance(obj, datetime.datetime): + serial = obj.isoformat() + return serial + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def remove_empty_from_dict(dictionary): + """ + Removes empty keys from dictionary + :param d: + :return: + """ + + try: + if isinstance(dictionary, dict): + return dict((k, remove_empty_from_dict(v)) for k, v in dictionary.items() \ + if v and remove_empty_from_dict(v)) + if isinstance(dictionary, list): + return [remove_empty_from_dict(v) for v in dictionary + if v and remove_empty_from_dict(v)] + + return dictionary + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +# def associate_zone(client, hosted_zone_id, region, vpc_id): +# """ +# Associates private hosted zone with VPC +# :param client: +# :param hosted_zone_id: +# :param region: +# :param vpc_id: +# :return: +# """ +# try: +# return client.associate_vpc_with_hosted_zone( +# HostedZoneId=hosted_zone_id, +# VPC={ +# 'VPCRegion': region, +# 'VPCId': vpc_id +# }, +# Comment='Updated by Lambda DDNS' +# ) +# except: +# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def is_dns_hostnames_enabled(client, vpc_id): + """ + Whether dns hostnames is enabled + :param client: + :param vpc_id: + :return: + """ + try: + response = client.describe_vpc_attribute( + Attribute='enableDnsHostnames', + VpcId=vpc_id + ) + + LOGGER.debug("%s", str(response) + lineno()) + return response['EnableDnsHostnames']['Value'] + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def is_dns_support_enabled(client, vpc_id): + """ + Whether dns support is enabled + :param client: + :param vpc_id: + :return: + """ + try: + response = client.describe_vpc_attribute( + Attribute='enableDnsSupport', + VpcId=vpc_id + ) + + LOGGER.debug('response2: %s', str(response) + lineno()) + return response['EnableDnsSupport']['Value'] + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def get_hosted_zone_properties(client, zone_id): + """ + Get hosted zone properties + :param client: + :param zone_id: + :return: + """ + try: + LOGGER.debug('getting hosted zone properties: zone_id: %s', str(zone_id) + lineno()) + hosted_zone_properties = client.get_hosted_zone(Id=zone_id) + LOGGER.debug('hosted_zone_properties: %s', str(hosted_zone_properties) + lineno()) + if 'ResponseMetadata' in hosted_zone_properties: + hosted_zone_properties.pop('ResponseMetadata') + return hosted_zone_properties + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def get_subnet_cidr_block(client, subnet_id): + """ + Get subnect cidr block + :param client: + :param subnet_id: + :return: + """ + try: + response = client.describe_subnets( + SubnetIds=[ + subnet_id + ] + ) + return response['Subnets'][0]['CidrBlock'] + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +# def publish_to_sns(client, account, region, message): +# """ +# Publish a simple message to the specified SNS topic +# :param client: +# :param account: +# :param region: +# :param message: +# :return: +# """ +# LOGGER.debug("SNS message: %s ", str(message)+lineno()) +# try: +# client.publish( +# TopicArn='arn:aws:sns:' + str(region) + ':' + str(account) + ':DDNSAlerts', +# Message=str(message) +# ) +# except ClientError as err: +# LOGGER.debug("Unexpected error: %s", str(err)+lineno()) + diff --git a/code/ddns-lambda.zip b/code/ddns-lambda.zip new file mode 100644 index 0000000..c129dd8 Binary files /dev/null and b/code/ddns-lambda.zip differ diff --git a/code/defaults.tf b/code/defaults.tf new file mode 120000 index 0000000..aeaa3fe --- /dev/null +++ b/code/defaults.tf @@ -0,0 +1 @@ +../defaults.tf \ No newline at end of file diff --git a/code/make-zip-file.tf b/code/make-zip-file.tf new file mode 100644 index 0000000..b4a1f39 --- /dev/null +++ b/code/make-zip-file.tf @@ -0,0 +1,23 @@ +locals { + lambda_file = format("%v.zip", local._defaults["lambda_file"]) + lambda_code_files = [ + "ddns-lambda.py", + ] + # this gets a sha256hash of each file, and then a sha256 hash of the comma-separated hashes. This will help determine + # to make a new zip file or not + lambda_code_files_hashes = { for f in local.lambda_code_files : f => filesha256(f) } + lambda_files_hash = sha256(join(",", values(local.lambda_code_files_hashes))) +} + +resource "null_resource" "zip" { + triggers = { + lambda_files_hash = local.lambda_files_hash + } + + provisioner "local-exec" { + command = "rm ${local.lambda_file}" + } + provisioner "local-exec" { + command = "zip ${local.lambda_file} -j -r ${join(" ", local.lambda_code_files)}" + } +} diff --git a/defaults.tf b/defaults.tf new file mode 100644 index 0000000..e1ee4cd --- /dev/null +++ b/defaults.tf @@ -0,0 +1,13 @@ +locals { + _defaults = { + "force_detach_policies" = false + "max_session_duration" = 3600 + "lambda_handler" = "ddns-lambda.lambda_handler" + "lambda_file" = "ddns-lambda" + "lambda_timeout" = 300 + "lambda_description" = "Take EC2 Events and register/deregister from Route53" + "cloudwatch" = { + "retention_in_days" = 180 + } + } +} diff --git a/dynamodb.tf b/dynamodb.tf new file mode 100644 index 0000000..918a220 --- /dev/null +++ b/dynamodb.tf @@ -0,0 +1,33 @@ +locals { + dynamodb_table_name = var.dynamodb_table_name != null ? var.dynamodb_table_name : local.name +} + +resource "aws_dynamodb_table" "table" { + count = var.create ? 1 : 0 + name = local.dynamodb_table_name + hash_key = "InstanceId" + billing_mode = "PROVISIONED" + read_capacity = 4 + write_capacity = 4 + + attribute { + name = "InstanceId" + type = "S" + } + + server_side_encryption { + enabled = true + } + + tags = merge( + local.base_tags, + var.tags, + lookup(var.component_tags, "ddb", {}), + map("Name", local.dynamodb_table_name), + ) + + lifecycle { + ignore_changes = [tags["boc:tf_module_version"]] + } + +} diff --git a/examples/test/test.tf b/examples/test/test.tf new file mode 100644 index 0000000..7e809d5 --- /dev/null +++ b/examples/test/test.tf @@ -0,0 +1,4 @@ +module "dynamic-route53" { + source = "git@github.e.it.census.gov:terraform-modules/aws-dynamic-route53.git?ref=initial" + +} diff --git a/lambda.tf b/lambda.tf new file mode 100644 index 0000000..b4486e0 --- /dev/null +++ b/lambda.tf @@ -0,0 +1,43 @@ +locals { + lambda_environment_variables = lookup(var.lambda_environment_variables, "DynamoDBName", null) != null ? var.lambda_environment_variables : merge( + var.lambda_environment_variables, + tomap({ "DynamoDBName" = local.dynamodb_table_name }), + ) + lambda_file = format("%v/code/%v.zip", path.module, local._defaults["lambda_file"]) +} + +resource "aws_lambda_function" "lambda" { + count = var.create ? 1 : 0 + function_name = local.lambda_name + description = local._defaults["lambda_description"] + handler = local._defaults["lambda_handler"] + memory_size = 128 + reserved_concurrent_executions = -1 + role = var.create ? aws_iam_role.role[0].arn : null + runtime = "python3.9" + source_code_hash = filebase64sha256(local.lambda_file) + filename = local.lambda_file + timeout = local._defaults["lambda_timeout"] + # version = "$LATEST" + + environment { + variables = local.lambda_environment_variables + } + timeouts {} + tracing_config { + mode = "PassThrough" + } + tags = merge( + local.base_tags, + var.tags, + map("Name", local.lambda_name) + ) +} + +resource "aws_lambda_alias" "lambda" { + count = var.create ? 1 : 0 + name = local.lambda_name + description = local._defaults["lambda_description"] + function_name = var.create ? aws_lambda_function.lambda[0].function_name : null + function_version = "$LATEST" +} diff --git a/locals.tf.initial b/locals.tf similarity index 75% rename from locals.tf.initial rename to locals.tf index 2bd4d7f..3f27853 100644 --- a/locals.tf.initial +++ b/locals.tf @@ -1,9 +1,12 @@ locals { account_id = var.account_id != "" ? var.account_id : data.aws_caller_identity.current.account_id account_environment = data.aws_arn.current.partition == "aws-us-gov" ? "gov" : "ew" + region = data.aws_region.current.name base_tags = { "boc:tf_module_version" = local._module_version "boc:created_by" = "terraform" } + + name = format("%v-%v", var.name, local.region) } diff --git a/role.tf b/role.tf new file mode 100644 index 0000000..d4bd1f0 --- /dev/null +++ b/role.tf @@ -0,0 +1,96 @@ +locals { + lambda_name = var.lambda_name != null ? var.lambda_name : local.name + lambda_policies = ["AWSLambdaBasicExecutionRole"] +} + +resource "aws_iam_role" "role" { + count = var.create ? 1 : 0 + description = "Lambda role for Dynamic Route53" + name = format("%v%v", local._prefixes["role"], local.lambda_name) + force_detach_policies = local._defaults["force_detach_policies"] + max_session_duration = local._defaults["max_session_duration"] + assume_role_policy = data.aws_iam_policy_document.lambda_assume.json + + inline_policy { + name = var.name + policy = data.aws_iam_policy_document.lambda_policy.json + } + + lifecycle { + ignore_changes = [tags["boc:tf_module_version"]] + } + + tags = merge( + local.base_tags, + var.tags, + lookup(var.component_tags, "role", {}), + tomap({ Name = format("%v%v", local._prefixes["role"], local.lambda_name) }), + ) +} + +resource "aws_iam_role_policy_attachment" "role" { + for_each = var.create ? toset([for k, v in data.aws_iam_policy.lambda_policies : v.arn]) : toset([]) + role = var.create ? aws_iam_role.role[0].name : "" + policy_arn = each.value +} + + +data "aws_iam_policy" "lambda_policies" { + for_each = toset(local.lambda_policies) + name = each.key +} + +data "aws_iam_policy_document" "lambda_policy" { + statement { + sid = "AllowRoute53" + effect = "Allow" + actions = [ + "route53:ListHostedZones*", + "route53:ListResourceRecordSets", + "route53:GetHostedZone*", + "route53:ChangeResourceRecordSets", + ] + resources = ["*"] + } + statement { + sid = "EC2" + effect = "Allow" + actions = ["ec2:Describe*"] + resources = ["*"] + } + statement { + sid = "DynamoDBGlobal" + effect = "Allow" + actions = ["dynamodb:ListTables"] + resources = ["*"] + } + statement { + sid = "DynamoDBTable" + effect = "Allow" + actions = [ + "dynamodb:BatchGet*", + "dynamodb:DeleteItem", + "dynamodb:Describe*", + "dynamodb:Get*", + "dynamodb:List*", + "dynamodb:PutItem", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:UpdateItem", + ] + resources = [var.create ? aws_dynamodb_table.table[0].arn : null] + } +} + +data "aws_iam_policy_document" "lambda_assume" { + statement { + sid = "LambdaAssumeRole" + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} diff --git a/variables.common.tf b/variables.common.tf index c77ef47..afbd2a7 100644 --- a/variables.common.tf +++ b/variables.common.tf @@ -24,3 +24,9 @@ variable "tags" { type = map(string) default = {} } + +variable "component_tags" { + description = "Additional tags for Components (s3, kms, ddb)" + type = map(map(string)) + default = { "s3" = {}, "kms" = {}, "ddb" = {} } +} diff --git a/variables.create.tf b/variables.create.tf new file mode 100644 index 0000000..7613cac --- /dev/null +++ b/variables.create.tf @@ -0,0 +1,5 @@ +variable "create" { + description = "Flag to indicate whether to create the resources or not (default: true)" + type = bool + default = true +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..fe23c08 --- /dev/null +++ b/variables.tf @@ -0,0 +1,30 @@ +variable "name" { + description = "Name to use within all the created resources (default: inf-dynamic-route53)" + type = string + default = "inf-dynamic-route53" +} + +variable "dynamodb_table_name" { + description = "Different DynamoDB table name to override default of var.name)" + type = string + default = null +} + +variable "lambda_name" { + description = "Different Lambda name to override default of var.name)" + type = string + default = null +} + +variable "lambda_environment_variables" { + description = "Map of lambda environment variables and values" + type = map(string) + default = { + SleepTime = 60 + DynamoDBName = null + TagKeyCname = "boc:dns:cname" + TagKeyZone = "boc:dns:zone" + TagKeyHostName = "TBD" + DNS_RR_TimeToLive = 60 + } +} diff --git a/version.tf b/version.tf index a0cd862..ac205ba 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.0" + _module_version = "0.0.16" } diff --git a/versions.tf b/versions.tf index 4ba10ce..34eb3b9 100644 --- a/versions.tf +++ b/versions.tf @@ -5,5 +5,5 @@ terraform { version = ">= 3.66.0" } } -# required_version = ">= 0.13" + # required_version = ">= 0.13" }