Search code examples
pythonamazon-web-servicesdnsaws-cloudformationamazon-route53

Automate DNS Query Logging with Route53 Resolver - Cloud Formation


So, I have about 200 accounts in AWS that need to have the VPC traffic queried and sent to an S3 bucket on a centralized account for monitoring - all VPCs in every account. We manually set up 5 accounts to test the function and it works as we expected. I decided to write up a yaml and deploy the script to all other accounts by means of CloudFormation. There's a problem - AWS has limited options for the Route53Resolver function where yaml is concerned.

I can assign the s3 destination and I can name the query log config, but that's it. The only think that makes that query log useful, associating the VPCs, cannot be done via CloudFormation. I have been tinkering with the cloud formation because it is truly only 10 lines of code:

Description: 
  Configure DNS Resolver Query Logging with all necessary resources

  Route53ResolverQuery-Config:
    Type: AWS::Route53Resolver::ResolverQueryLoggingConfig
    Properties: 
      DestinationArn: *destination*
      Name: route53-dns-query-logging

It is simply missing one single option to add the VPCs that are to be queried. According to the AWS documentation, it is not included:

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53resolver-resolverqueryloggingconfigassociation.html

Now, I know the alternative, because boto3 has the solution in associating a VPC:

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/route53resolver.html#Route53Resolver.Client.associate_resolver_query_log_config

So, I could create the config log, build a lambda function, build in a maintenance window and the necessary roles and policies to run them, bake it all into a yaml and deploy a stackset to all child accounts and call it good, but that seems like too much extra work for something that could absolutely be done with just one more available property in the yaml function.

So, my question to you all is - is there anything I'm missing that could round out this code simply in a single yaml resource? Or as close to it as possible?


Solution

  • I ended up leveraging a Lambda function for this, as I thought I might have to, but I wanted to post the answer for anyone curious:

    I polished the script to cover some information for confidentiality:

    Description:
      Configure DNS Resolver Query Logging and create lambda and maintenance window with necessary permissions to associate VPCs with Route 53 Resolver Query Log Config.
    
    
    #
    #    Creating variables to be used within the script to swap out necessary bucket drops
    #    and dependecy information for WatiCondition
    #
    Parameters:
      CommercialMaster:
        Description: The Commercial Account ID for routing.
        Type: String
        Default: ###########
      GovCloudMaster:
        Description: The GovCloud Account ID for routing.
        Type: String
        Default: ###########
    
    
    #
    #    Creating conditional statements to be used within the script for necessary function
    #    to create the Global and Region-specific resources.
    #
    Conditions:
      Commercial: !Equals [ !Ref AWS::Partition, 'aws' ]
     
    
    Resources:
    #
    #    Route 53 Query Logging config set up
    #
      Route53ResolverQueryConfig:
        Type: AWS::Route53Resolver::ResolverQueryLoggingConfig
        Properties:
          DestinationArn: !If [ Commercial, !Sub "arn:${AWS::Partition}:s3:::route53-logs-${CommercialMaster}", !Sub "arn:${AWS::Partition}:s3:::route53-logs-${GovCloudMaster}/AWSLogs" ]
          Name: route53-dns-query-logging
     
    #
    #    Necessary roles and policies for Lambda function and Maintenance Window task
    #
      LambdaPermissionsRole:
        Type: "AWS::IAM::Role"
        Properties:
          RoleName: LambdaR53Role
          Description:  Lambda permissions role for R53 association.
          AssumeRolePolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Principal:
                  Service:
                    - lambda.amazonaws.com
                Action:
                  - sts:AssumeRole
          Path: /
          Policies:
            - PolicyName: LambdaR53Policy
              PolicyDocument:
                Version: 2012-10-17
                Statement:
                  - Effect: Allow
                    Action:
                      - logs:PutResourcePolicy
                      - logs:DescribeLogGroups
                      - route53resolver:ListResolverQueryLogConfigAssociations
                      - route53resolver:AssociateResolverQueryLogConfig
                      - logs:UpdateLogDelivery
                      - ec2:DescribeVpcs
                      - route53resolver:ListResolverQueryLogConfigs
                      - logs:DescribeResourcePolicies
                      - logs:GetLogDelivery
                      - logs:ListLogDeliveries
                    Resource: "*"
     
      MaintenanceWindowPermissionsRole:
        Type: AWS::IAM::Role
        Properties:
          RoleName: SSMMaintenanceTaskRole
          AssumeRolePolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Principal:
                  Service:
                    - ssm.amazonaws.com
                Action:
                  - sts:AssumeRole
          Path: /  
          Policies:
            - PolicyName: SSMMaintenanceTaskPolicy
              PolicyDocument:
                Version: "2012-10-17"
                Statement:
                  - Effect: "Allow"
                    Action: "lambda:InvokeFunction"
                    Resource: !Sub arn:${AWS::Partition}:lambda:*:${AWS::AccountId}:function:*
     
    #
    #    Lambda function to check for new VPCs and associate them to the query log config.
    #
      AssociateVPCs:
        Type: 'AWS::Lambda::Function'
        DependsOn: WaitCondition
        Properties:
          Code:
            ZipFile: |
                import boto3
                import json
     
                def lambda_handler(event, context):
                    client = client_obj()
                    associated = associated_list(client)
                    response = client.list_resolver_query_log_configs(
                        Filters=[
                            {
                                'Name': 'Name',
                                'Values': [
                                    'route53-dns-query-logging',
                                ]
                            }
                        ]
                    )
                    config = response['ResolverQueryLogConfigs'][0]['Id']
                    ec2 = boto3.client('ec2')
                    vpc = ec2.describe_vpcs()
                    vpcs = vpc['Vpcs']
     
                    for v in vpcs:
                        if v['VpcId'] not in associated:
                            client.associate_resolver_query_log_config(
                                ResolverQueryLogConfigId= f"{config}",
                                ResourceId=f"{v['VpcId']}"
                            )
                            print(f"{v['VpcId']} has been connected for monitoring.")
                        else:
                            print(f"{v['VpcId']} is already linked.")
     
                def client_obj():
                    client = boto3.client('route53resolver')
                    return client
     
                def associated_list(client_object):
                    associated = list()
                    assoc = client_object.list_resolver_query_log_config_associations()
                    for element in assoc['ResolverQueryLogConfigAssociations']:
                        associated.append(element['ResourceId'])
                    return associated
          Description: This function associates VPCs with a DNS Query Logging Config for monitoring
          FunctionName: R53-Resolver-Config-Association
          Handler: index.lambda_handler
          Role: !GetAtt
            - LambdaPermissionsRole
            - Arn
          Runtime: python3.9
          Timeout: 600
     
    #
    #    Maintenance window and Task that triggers the Lambda function every day at 2:00am (customer requested)
    #
      MaintenanceWindowTrigger:
        Type: AWS::SSM::MaintenanceWindow
        Properties:
          Name: R53-QueryVPC-Associations
          AllowUnassociatedTargets: True
          Description: Associate VPCs to Route53 Resolver Query Logging Config
          Duration: 3
          Cutoff: 1
          Schedule: cron(0 0 2 ? * * *)
          ScheduleTimezone: US/Eastern
     
      MaintenanceWindowLambdaTask:
        Type: AWS::SSM::MaintenanceWindowTask
        Properties:
          WindowId: !Ref MaintenanceWindowTrigger
          TaskArn:
            "Fn::GetAtt": [AssociateVPCs, Arn]
          TaskType: LAMBDA
          Priority: 1
          Name: Daily-VPC-Association-Route53
          ServiceRoleArn:
            "Fn::GetAtt": [MaintenanceWindowPermissionsRole, Arn]