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"
}