diff --git a/roles/aws/aws_admin_tools/templates/api_get_list_of_ec2.py.j2 b/roles/aws/aws_admin_tools/templates/api_get_list_of_ec2.py.j2 deleted file mode 100644 index a3af97ec6..000000000 --- a/roles/aws/aws_admin_tools/templates/api_get_list_of_ec2.py.j2 +++ /dev/null @@ -1,48 +0,0 @@ -import json -import boto3 - -# Defining Clients -ec2_cli = boto3.client("ec2", region_name="{{ _aws_region }}") - -def lambda_handler(event, context): - print("Gathering instance details.") - - # Describe instances and addresses once - instances_response = ec2_cli.describe_instances() - addresses_response = ec2_cli.describe_addresses() - - # Preprocess EIPs for quick lookup by tag:Name - eip_map = {} - for eip in addresses_response.get('Addresses', []): - name_tag = next((tag['Value'] for tag in eip.get('Tags', []) if tag['Key'] == 'Name'), None) - if name_tag: - eip_map.setdefault(name_tag, {'Public': [], 'Private': []}) - if 'PublicIp' in eip: - eip_map[name_tag]['Public'].append(eip['PublicIp']) - if 'PrivateIpAddress' in eip: - eip_map[name_tag]['Private'].append(eip['PrivateIpAddress']) - - ec2_info_list = [] - - for reservation in instances_response.get("Reservations", []): - for instance in reservation.get("Instances", []): - inst_name = "-" - if "Tags" in instance: - for tag in instance["Tags"]: - if tag["Key"] == "Name": - inst_name = tag["Value"] - break - - ec2_info_list.append({ - "EC2Name": inst_name, - "State": instance.get("State", {}), - "PublicIP": instance.get("PublicIpAddress", "-"), - "PrivateIP": instance.get("PrivateIpAddress", "-"), - "InstanceType": instance.get("InstanceType", "-"), - "EIP": eip_map.get(inst_name, {"Public": [], "Private": []}) - }) - - return { - "statusCode": 200, - "EC2Info": ec2_info_list - } diff --git a/roles/aws/aws_admin_tools/templates/default_s3_object.j2 b/roles/aws/aws_admin_tools/templates/default_s3_object.j2 deleted file mode 100644 index 5dfe8fb2b..000000000 --- a/roles/aws/aws_admin_tools/templates/default_s3_object.j2 +++ /dev/null @@ -1 +0,0 @@ -Hello from S3! diff --git a/roles/aws/aws_admin_tools/templates/get_infra_data_from_s3.py.j2 b/roles/aws/aws_admin_tools/templates/get_infra_data_from_s3.py.j2 deleted file mode 100644 index e69de29bb..000000000 diff --git a/roles/aws/aws_backup_validation/defaults/main.yml b/roles/aws/aws_backup_validation/defaults/main.yml index bbbfa3282..d128e7f18 100644 --- a/roles/aws/aws_backup_validation/defaults/main.yml +++ b/roles/aws/aws_backup_validation/defaults/main.yml @@ -8,6 +8,46 @@ aws_backup_validation: runtime: "python3.12" handler: "lambda_handler" resources: - - EC2 - - RDS + - name: ec2_test_instance + git_url: git@gitlab.dummy-infra1.codeenigma.net:functions/ec2_test_instance.git + type: EC2 + lambda_policy: + - "backup:PutRestoreValidationResult" + - "ssm:GetCommandInvocation" + - "ssm:GetConnectionStatus" + - "ssm:SendCommand" + - "ec2:DescribeInstances" + - name: rds_test_instance + git_url: git@gitlab.dummy-infra1.codeenigma.net:functions/rds_test_instance.git + type: RDS + lambda_policy: + - "backup:PutRestoreValidationResult" + - "ssm:GetCommandInvocation" + - "ssm:SendCommand" + - "ec2:DescribeInstances" + - "rds:DescribeDBInstances" + - name: aurora_create_instance + git_url: git@gitlab.dummy-infra1.codeenigma.net:functions/aurora_create_instance.git + type: Aurora + lambda_policy: + - "lambda:InvokeFunction" + - name: aurora_test_instance + git_url: git@gitlab.dummy-infra1.codeenigma.net:functions/aurora_test_instance.git + type: Aurora + event_pattern: '{ "source": ["aws.rds"], "detail-type": ["RDS DB Instance Event"], "resources": [{ "prefix": "arn:aws:rds:eu-west-1:{{ _acc_id }}:db:restoretest" }], "detail": { "EventID": ["RDS-EVENT-0005"] } }' + lambda_policy: + - "backup:PutRestoreValidationResult" + - "ec2:DescribeInstances" + - "rds:DescribeDBInstances" + - "rds:DescribeDBClusters" + - "rds:DeleteDBInstance" + - name: validation_report + git_url: git@gitlab.example.codeenigmma.net:functions/validation_report.git + type: Schedule + schedule: "cron(0 0 ? * MON *)" + lambda_policy: + - "backup:ListRestoreJobs" + - "ses:SendEmail" + - "ec2:DescribeImages" + - "rds:DescribeDbSnapshots" #- EFS diff --git a/roles/aws/aws_backup_validation/files/trusted_entitites.j2 b/roles/aws/aws_backup_validation/files/trusted_entitites.j2 deleted file mode 100644 index fb84ae9de..000000000 --- a/roles/aws/aws_backup_validation/files/trusted_entitites.j2 +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] -} diff --git a/roles/aws/aws_backup_validation/tasks/main.yml b/roles/aws/aws_backup_validation/tasks/main.yml index b4ab85c2a..e1d51495c 100644 --- a/roles/aws/aws_backup_validation/tasks/main.yml +++ b/roles/aws/aws_backup_validation/tasks/main.yml @@ -1,36 +1,100 @@ --- -- name: Create a role and attach policies for Lambda backup validation. +- name: Get account ID for ARN. + ansible.builtin.command: >- + aws sts get-caller-identity + --query Account + --output text + register: _acc_id + +- name: Setting previous command output into variable. + ansible.builtin.set_fact: + _acc_id: "{{ _acc_id.stdout | from_json }}" + +# Passing Api throug the resource ( triger ) + +- name: Configure API Gateway if defined. + ansible.builtin.include_tasks: create_mock.yml + when: _api_index | length == 0 + +- name: Create a role and attach policies for events. ansible.builtin.include_role: name: aws/aws_iam_role vars: aws_iam_role: - name: LambdaBackupRestoreRole + name: "{{ item.name }}_event" + source: "{{ item.name}}" + aws_profile: "{{ _aws_profile }}" + inline_policies: + name: "{{ item.name }}_event" + resource: "arn:aws:lambda:{{ _aws_region }}:{{ _acc_id }}:function:{{ item.name }}" + action: + - "lambda:InvokeFunction" + policy_document: "{{ lookup('template', 'event_document_policy.json.j2') }}" + loop: "{{ aws_backup_validation.resources }}" + loop_control: + extended: true + extended_allitems: false + +- name: Create a role and attach policies for Lambda functions. + ansible.builtin.include_role: + name: aws/aws_iam_role + vars: + aws_iam_role: + name: "{{ item.name}}_lambda" + source: "{{ item.name}}" aws_profile: "{{ _aws_profile }}" managed_policies: - - arn:aws:iam::aws:policy/AmazonEC2FullAccess - - arn:aws:iam::aws:policy/AWSBackupFullAccess - - arn:aws:iam::aws:policy/AmazonRDSFullAccess - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - - arn:aws:iam::aws:policy/AmazonSSMFullAccess - - arn:aws:iam::aws:policy/AmazonSESFullAccess - policy_document: "{{ lookup('file', 'trusted_entitites.j2') }}" + inline_policies: + name: "{{ item.name }}_lambda" + resource: "*" + action: "{{ item.lambda_policy }}" + policy_document: "{{ lookup('template', 'trusted_entitites.json.j2') }}" + loop: "{{ aws_backup_validation.resources }}" + loop_control: + extended: true + extended_allitems: false + +- name: Get info about newly created restore testing plan. + ansible.builtin.command: > + aws backup list-restore-testing-plans --region {{ _aws_region }} + register: _testing_plans + +- name: Create Lambda functions from templates. + ansible.builtin.include_role: + name: aws/aws_lambda + vars: + aws_lambda: + name: "{{ item.name }}" + description: "Lambda functions for {{ item.type }} validation." + timeout: "{{ aws_backup_validation.timeout }}" + role: "{{ aws_iam_role._result[item.name + '_lambda'] }}" + runtime: "{{ aws_backup_validation.runtime }}" + function_file: "{{ lookup('template', item.name + '.py.j2') }}" + s3_bucket: "{{ aws_backup_validation.s3_bucket }}" + s3_bucket_prefix: "lambda-functions" + tags: + Name: "{{ item.name }}" + loop: "{{ aws_backup_validation.resources }}" + when: item.git_url is not defined -- name: Create backup validation Lambda functions. +- name: Create Lambda functions from git url. ansible.builtin.include_role: name: aws/aws_lambda vars: aws_lambda: - name: "{{ aws_backup_validation.name }}_{{ item }}" - description: "{{ aws_backup_validation.description }}" + name: "{{ item.name }}" + description: "Lambda functions for {{ item.type }} validation." timeout: "{{ aws_backup_validation.timeout }}" - role: "{{ aws_iam_role._result['LambdaBackupRestoreRole'] }}" + role: "{{ aws_iam_role._result[item.name + '_lambda'] }}" runtime: "{{ aws_backup_validation.runtime }}" - function_file: "{{ lookup('template', item + '_validation.py.j2') }}" + function_file: "{{ item.git_url }}" s3_bucket: "{{ aws_backup_validation.s3_bucket }}" s3_bucket_prefix: "lambda-functions" tags: - Name: "{{ item }}_backup_validation" + Name: "{{ item.name }}" loop: "{{ aws_backup_validation.resources }}" + when: item.git_url is defined - name: Create an IAM Managed Policy for passing roles and setup IAM role. ansible.builtin.include_role: @@ -53,79 +117,52 @@ #- name: Get verified domain. # ansible.builtin.include_tasks: get_valid_email.yml -- name: Get info about newly created restore testing plan. - ansible.builtin.command: > - aws backup list-restore-testing-plans --region {{ _aws_region }} - register: _testing_plans - -- name: Create validation report function. - ansible.builtin.include_role: - name: aws/aws_lambda - vars: - aws_lambda: - name: "validation_report" - description: "{{ aws_backup_validation.description }}" - timeout: "30" - role: "{{ aws_iam_role._result['LambdaBackupRestoreRole'] }}" - runtime: "{{ aws_backup_validation.runtime }}" - function_file: "{{ lookup('template', 'validation_report.py.j2') }}" - s3_bucket: "{{ aws_backup_validation.s3_bucket }}" - s3_bucket_prefix: "lambda-functions" - tags: - Name: "validation_report" - -- name: Get account ID for ARN. - ansible.builtin.command: >- - aws sts get-caller-identity - --query Account - --output text - register: _acc_id - -- name: Setting previous command output into variable. - ansible.builtin.set_fact: - _acc_id: "{{ _acc_id.stdout | from_json }}" - -- name: Create EventBridge for validation functions. +- name: Create EventBridge with lambda functions. amazon.aws.cloudwatchevent_rule: - name: "RestoreValidation_{{ item }}" + name: "{{ item.name }}" description: "{{ aws_backup_validation.description }}" state: present region: "{{ _aws_region }}" - event_pattern: '{ "source": ["aws.backup"], "detail-type": ["Restore Job State Change"], "detail": { "resourceType": ["{{ item }}"], "status": ["COMPLETED"] } }' + role_arn: "arn:aws:iam::{{ _acc_id }}:role/{{ item.name }}_event" + event_pattern: >- + {{ item.event_pattern | default( + { + "source": ["aws.backup"], + "detail-type": ["Restore Job State Change"], + "detail": { + "resourceType": [ item.type ], + "status": ["COMPLETED"] + } + } | to_json + ) }} targets: - - id: "RestoreValidation_{{ item }}" - arn: "arn:aws:lambda:{{ _aws_region }}:{{ _acc_id }}:function:RestoreValidation_{{ item }}" + - id: "{{ item.name }}" + arn: "arn:aws:lambda:{{ _aws_region }}:{{ _acc_id }}:function:{{ item.name }}" loop: "{{ aws_backup_validation.resources }}" + when: item.type != "Schedule" register: _event_bridges - name: Create schedule for validation reports. amazon.aws.cloudwatchevent_rule: - name: validation_report - schedule_expression: "cron(0 0 ? * MON *)" - description: Run validation reporting + name: "{{ item.name }}" + schedule_expression: "{{ item.schedule }}" + description: "Run validation reporting." region: "{{ _aws_region }}" + role_arn: "arn:aws:iam::{{ _acc_id }}:role/{{ item.name }}_event" targets: - id: validation_report - arn: "{{ (aws_lambda._result['validation_report'].configuration.function_arn.split(':') | map('trim'))[:-1] | join(':') }}" # Remove the version number from ARN + arn: "arn:aws:lambda:{{ _aws_region }}:{{ _acc_id }}:function:{{ item.name }}" + loop: "{{ aws_backup_validation.resources }}" + when: item.type == "Schedule" register: _validation_event -- name: Update Lambda policy. +- name: Update Lambda policies. amazon.aws.lambda_policy: state: present - function_name: "{{ item.rule.name }}" - statement_id: "{{ item.rule.name }}" + function_name: "{{ item.name }}" + statement_id: "{{ item.name }}" action: lambda:InvokeFunction principal: events.amazonaws.com - source_arn: "{{ item.rule.arn }}" - region: "{{ _aws_region }}" - loop: "{{ _event_bridges.results }}" - -- name: Update lambda validation report policy. - amazon.aws.lambda_policy: - state: present - function_name: "validation_report" - statement_id: "validation_report" - action: lambda:InvokeFunction - principal: events.amazonaws.com - source_arn: "{{ _validation_event.rule.arn }}" + source_arn: "arn:aws:events:{{ _aws_region }}:{{ _acc_id }}:rule/{{ item.name }}" region: "{{ _aws_region }}" + loop: "{{ aws_backup_validation.resources }}" diff --git a/roles/aws/aws_backup_validation/tasks/testing_resources.yml b/roles/aws/aws_backup_validation/tasks/testing_resources.yml index 3e996ec22..d95ae4295 100644 --- a/roles/aws/aws_backup_validation/tasks/testing_resources.yml +++ b/roles/aws/aws_backup_validation/tasks/testing_resources.yml @@ -68,34 +68,26 @@ instance: "EC2" file-system: "EFS" db: "RDS" - cluster: "AURORA" + cluster: "Aurora" - name: Set instance type for template. ansible.builtin.set_fact: _instance_type_restore: "{{ instance_type[backup.resource_type] }}" - _template_prefix: "{{ instance_type[backup.resource_type] }}" when: backup.resource_type != 'file-system' -- name: Set instance type to Aurora if defined. - ansible.builtin.set_fact: - _instance_type_restore: "Aurora" - when: - - backup.resource_type == 'db' - - "'aurora' in aws_rds.engine" - - name: Create restore testing query file. ansible.builtin.template: - src: "{{ _template_prefix }}_restore_testing.j2" + src: "{{ _instance_type_restore }}_restore_testing.j2" dest: /tmp/restore_testing.json register: _restore_testing_query - when: _template_prefix is defined + when: _instance_type_restore is defined - name: Check if protected reource exist. ansible.builtin.command: > aws backup list-protected-resources --query "Results[?ResourceArn=='{{ _resource_arn }}']" --region {{ _aws_region }} register: _protected_res -- name: Assign {{ _template_prefix }} resource to AWS restore testing plan. +- name: Assign {{ _instance_type_restore }} resource to AWS restore testing plan. ansible.builtin.command: > aws backup create-restore-testing-selection --cli-input-json file:///tmp/restore_testing.json --region {{ _aws_region }} - when: _template_prefix is defined and _testing_plan_info.stdout != "null" and _testing_selection_exists.stdout | length == 0 and _protected_res.stdout | length != 0 + when: _instance_type_restore is defined and _testing_plan_info.stdout != "null" and _testing_selection_exists.stdout | length == 0 and _protected_res.stdout | length != 0 diff --git a/roles/aws/aws_backup_validation/templates/AURORA_restore_testing.j2 b/roles/aws/aws_backup_validation/templates/Aurora_restore_testing.j2 similarity index 100% rename from roles/aws/aws_backup_validation/templates/AURORA_restore_testing.j2 rename to roles/aws/aws_backup_validation/templates/Aurora_restore_testing.j2 diff --git a/roles/aws/aws_backup_validation/templates/EC2_validation.py.j2 b/roles/aws/aws_backup_validation/templates/EC2_validation.py.j2 deleted file mode 100644 index 70b0963b7..000000000 --- a/roles/aws/aws_backup_validation/templates/EC2_validation.py.j2 +++ /dev/null @@ -1,117 +0,0 @@ -import json -import boto3 -import socket -import time - -# Defining Clients -#s3_cli = boto3.client('s3', region_name='eu-west-2') -backup_cli = boto3.client('backup', region_name="{{ _aws_region }}") -ec2_cli = boto3.client("ec2", region_name="{{ _aws_region }}") -ssm_cli = boto3.client('ssm', region_name="{{ _aws_region }}") -ses_cli = boto3.client('ses', region_name="{{ _aws_region }}") - -# Debugger -#boto3.set_stream_logger('') - -def lambda_handler(event, context): - - mail_body = "" - - print("Gathering instance id.") - ec2_instance_id=event['detail']['createdResourceArn'].split("/",1)[1] - - print("Gathering instance details.") - ec2_instances=ec2_cli.describe_instances() - - instance_exist = False - - for reservation in ec2_instances["Reservations"]: - for instance in reservation["Instances"]: - if ec2_instance_id == instance["InstanceId"]: - tags = instance['Tags'] - instance_type = instance["InstanceType"] - private_ip = instance["PrivateIpAddress"] - mail_body = mail_body + "Instance is restored!\n" - instance_exist = True - - if instance_exist: - port = 22 - - print("Gathering instance name.") - instance_name = '' - for tag in tags: - if tag['Key'] == 'Name': - instance_name = tag['Value'] - - print("Testing connection!") - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(5.0) - result = sock.connect_ex((private_ip,port)) - - print(result) - - # If result was not success - if result != 0: - mail_body = mail_body + "Connection on " + str(port) + " is not working, this could be caused by firewall not accepting connections.\n" - else: - mail_body = mail_body + "Connection on " + str(port) + " is working!\n" - - # Check if SSM is set up for instance - ssm_status = ssm_cli.get_connection_status(Target=ec2_instance_id) - - if ssm_status['Status'] == 'connected': - # Run scripts on EC2 - print("Creating script!") - script = """ - echo "Server info:" - hostnamectl - echo "Disk usage:" - df -h - """ - - print("Running command!") - command_response = ssm_cli.send_command( - DocumentName ='AWS-RunShellScript', - Parameters = {'commands': [script]}, - InstanceIds = [ - ec2_instance_id - ] - ) - - print("Gathering commands details!") - time.sleep(10) - c_res = ssm_cli.get_command_invocation( - CommandId=command_response['Command']['CommandId'], - InstanceId=ec2_instance_id - ) - - print(c_res['StandardOutputContent']) - instance_message = "Instance " + instance_name + " - " + ec2_instance_id + " was restored.\n" - mail_body = mail_body + "SSM is working and these are the details of the instance:\n" + c_res['StandardOutputContent'] - - else: - mail_body = mail_body + "SSM is not configured or accessible!\n" - - print("Validating Restore job!") - backup_cli.put_restore_validation_result( - RestoreJobId=event['detail']['restoreJobId'], - ValidationStatus="SUCCESSFUL", - ValidationStatusMessage="" - ) - - else: - print("Validating Restore job!") - backup_cli.put_restore_validation_result( - RestoreJobId=event['detail']['restoreJobId'], - ValidationStatus="FAILED", - ValidationStatusMessage="" - ) - - mail_body = mail_body + "Instance " + ec2_instance_id + " is not running!" - - print(instance_message + mail_body) - - return { - 'statusCode': 200, - 'body': event - } diff --git a/roles/aws/aws_backup_validation/templates/RDS_validation.py.j2 b/roles/aws/aws_backup_validation/templates/RDS_validation.py.j2 deleted file mode 100644 index a5c2daab3..000000000 --- a/roles/aws/aws_backup_validation/templates/RDS_validation.py.j2 +++ /dev/null @@ -1,97 +0,0 @@ -import json -import boto3 -import socket -import time - -# Defining Clients -#s3_cli = boto3.client('s3', region_name='eu-west-2') -backup_cli = boto3.client('backup', region_name="{{ _aws_region }}") -ec2_cli = boto3.client("ec2", region_name="{{ _aws_region }}") -rds_cli = boto3.client("rds", region_name="{{ _aws_region }}") -ssm_cli = boto3.client('ssm', region_name="{{ _aws_region }}") -ses_cli = boto3.client('ses', region_name="{{ _aws_region }}") - -# Debugger -#boto3.set_stream_logger('') - -def lambda_handler(event, context): - - mail_body = "" - - print("Gathering instance id.") - rds_instance_id=event['detail']['createdResourceArn'].split(":")[-1] - - print(rds_instance_id) - - print("Gathering instance details.") - rds_instance=rds_cli.describe_db_instances( - DBInstanceIdentifier=rds_instance_id - ) - - print("Get instance name with access to RDS") - for tag in rds_instance['DBInstances'][0]['TagList']: - if tag['Key']=='Name': - ec2_with_access=tag['Value'] - - new_endpoint = rds_instance['DBInstances'][0]['Endpoint']['Address'] - - print(ec2_with_access) - - print("Get all instances") - ec2_instances=ec2_cli.describe_instances() - - instance_exist = False - - print("Iterate to get id of EC2") - for reservation in ec2_instances["Reservations"]: - for instance in reservation["Instances"]: - for tag in instance['Tags']: - if ((tag['Key'] == "Name") and (tag['Value'] == ec2_with_access)): - print(tag['Key'] + " - " + tag['Value'] + " - " + instance["InstanceId"]) - ec2_instance_id = instance["InstanceId"] - - print(ec2_instance_id) - - # Run scripts on EC2 - print("Creating script!") - script = """ - old_host={{ '$' }}(grep host /home/deploy/.mysql.creds | awk -F= '{{ '{{' }}print $2 {{ '}}' }}') - sed 's,old_host,{new_host},g' /home/deploy/.mysql.creds >> /home/deploy/.mysql.creds.tmp - mysql --defaults-file=/home/deploy/.mysql.creds.tmp -e "SELECT table_schema 'DB Name', ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) 'DB Size in MB' FROM information_schema.tables GROUP BY table_schema;" - rm /home/deploy/.mysql.creds.tmp - """.format(new_host=new_endpoint) - - print("Running command!") - command_response = ssm_cli.send_command( - DocumentName ='AWS-RunShellScript', - Parameters = {'commands': [script]}, - InstanceIds = [ - ec2_instance_id - ] - ) - - print("Getting command output.") - time.sleep(30) - c_res = ssm_cli.get_command_invocation( - CommandId=command_response['Command']['CommandId'], - InstanceId=ec2_instance_id - ) - - print(c_res['StandardOutputContent']) - instance_message = "RDS instance " + ec2_with_access + " was restored.\n" - conn_message = "Able to get information from " + new_endpoint + ":\n" - mail_body=instance_message + conn_message + c_res['StandardOutputContent'] - - print("Validating Restore job!") - backup_cli.put_restore_validation_result( - RestoreJobId=event['detail']['restoreJobId'], - ValidationStatus="SUCCESSFUL", - ValidationStatusMessage="" - ) - - print(mail_body) - - return { - 'statusCode': 200, - 'body': json.dumps(event) - } diff --git a/roles/aws/aws_backup_validation/templates/event_document_policy.json.j2 b/roles/aws/aws_backup_validation/templates/event_document_policy.json.j2 new file mode 100644 index 000000000..f76d3d804 --- /dev/null +++ b/roles/aws/aws_backup_validation/templates/event_document_policy.json.j2 @@ -0,0 +1,19 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "TrustEventBridgeService", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "aws:SourceArn": "arn:aws:events:{{ _aws_region }}:{{ _acc_id }}:rule/{{ aws_iam_role.source }}", + "aws:SourceAccount": "{{ _acc_id }}" + } + } + } + ] +} diff --git a/roles/aws/aws_backup_validation/templates/trusted_entitites.json.j2 b/roles/aws/aws_backup_validation/templates/trusted_entitites.json.j2 new file mode 100644 index 000000000..d6c4d434a --- /dev/null +++ b/roles/aws/aws_backup_validation/templates/trusted_entitites.json.j2 @@ -0,0 +1,18 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "aws:SourceArn": "arn:aws:lambda:{{ _aws_region }}:{{ _acc_id }}:function:{{ aws_iam_role.source }}", + "aws:SourceAccount": "{{ _acc_id }}" + } + } + } + ] +} diff --git a/roles/aws/aws_backup_validation/templates/validation_report.py.j2 b/roles/aws/aws_backup_validation/templates/validation_report.py.j2 deleted file mode 100644 index 7501db7ca..000000000 --- a/roles/aws/aws_backup_validation/templates/validation_report.py.j2 +++ /dev/null @@ -1,138 +0,0 @@ -import json -import boto3 -import socket -import time -import datetime - -# Defining Clients -backup_cli = boto3.client('backup', region_name="{{ _aws_region }}") -ses_cli = boto3.client('ses', region_name="{{ _aws_region }}") -ec2_cli = boto3.client('ec2', region_name="{{ _aws_region }}") -rds_cli = boto3.client('rds', region_name="{{ _aws_region }}") - -mail_title = "" -mail_body = "" - -# Debugger -#boto3.set_stream_logger('') - -def set_mail_body(success_restore, inst, instance_name, ami_id): - global mail_body - if success_restore: - mail_body+=instance_name + " was restored successfully from " + ami_id + ", " - else: - mail_body+=instance_name + " failed to restore from " + ami_id + ", " - - if inst['ValidationStatus'] == "SUCCESSFUL": - mail_body+= "and validation was successful! \n" - elif inst['ValidationStatus'] == "FAILED": - failed_validation=True - mail_body+= "but validation has failed with following message: \n" - mail_body+= inst['ValidationStatusMessage'] - elif inst['ValidationStatus'] == "TIMED_OUT": - failed_validation=True - mail_body+= "but validation timed out. \n" - else: - failed_validation=True - mail_body+="with unknown validation failure! \n" - -def lambda_handler(event, context): - instance_name = "" - success_restore = False - failed_validation = False - global mail_body - last_restore_valdation_date = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') - completed_jobs=[] - failed_jobs=[] - - print("Getting list of successful restoration.") - {% for plan in _testing_plans.stdout | from_json | json_query('RestoreTestingPlans') %} -completed_job = backup_cli.list_restore_jobs( - ByCreatedAfter=last_restore_valdation_date, - ByStatus='COMPLETED', - ByRestoreTestingPlanArn='{{ plan['RestoreTestingPlanArn'] }}' - ) - completed_jobs+=completed_job['RestoreJobs'] - {% endfor %} - - print("Getting instance details.") - for inst in completed_jobs: - success_restore = True - print("Getting instance arn.") - ami_arn = inst['RecoveryPointArn'].split(':')[2] - - if ami_arn == 'ec2': - ami_id = inst['RecoveryPointArn'].split('/')[-1] - instance_details = ec2_cli.describe_images( - ImageIds=[ami_id] - ) - for tag in instance_details['Images'][0]['Tags']: - if tag['Key'] == 'Name': - instance_name = tag['Value'] - - elif ami_arn == 'rds': - ami_id = inst['RecoveryPointArn'].split('snapshot:')[-1] - instance_details = rds_cli.describe_db_snapshots( - DBSnapshotIdentifier=ami_id - ) - print(instance_details) - for tag in instance_details['DBSnapshots'][0]['TagList']: - if tag['Key'] == 'Name': - instance_name = tag['Value'] - - else: - ami_id = 'Probably EFS' - instance_name = 'latest snapshot' - - set_mail_body(success_restore, inst, instance_name, ami_id) - - print(mail_body) - print("Getting list of failed restoration.") - {% for plan in _testing_plans.stdout | from_json | json_query('RestoreTestingPlans') %} -failed_job = backup_cli.list_restore_jobs( - ByCreatedAfter=last_restore_valdation_date, - ByStatus='FAILED', - ByRestoreTestingPlanArn='{{ plan['RestoreTestingPlanArn'] }}' - ) - failed_jobs += failed_job['RestoreJobs'] - {% endfor %} - - if len(failed_jobs) > 0: - mail_title = "🔴 Failed!" - else: - mail_title = "🟢 Success!" - print("Successful restore jobs:") - print(completed_jobs) - - print("Failed restore jobs:") - print(failed_jobs) - - print("Sending email!") - response = ses_cli.send_email( - Destination={ - 'BccAddresses': [ - ], - 'CcAddresses': [], - 'ToAddresses': [ - 'sysadm@codeenigma.com' - ], - }, - Message={ - 'Body': { - 'Text': { - 'Charset': 'UTF-8', - 'Data': mail_body, - }, - }, - 'Subject': { - 'Charset': 'UTF-8', - 'Data': 'Restore testing - {{ _infra_name }}: ' + mail_title, - }, - }, - Source='Lambda Backup Validation ', - ) - - return { - 'statusCode': 200, - 'body': event - } diff --git a/roles/aws/aws_iam_role/files/lambda_document_policy.json b/roles/aws/aws_iam_role/files/lambda_document_policy.json new file mode 100644 index 000000000..fd267525d --- /dev/null +++ b/roles/aws/aws_iam_role/files/lambda_document_policy.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} diff --git a/roles/aws/aws_iam_role/tasks/main.yml b/roles/aws/aws_iam_role/tasks/main.yml index fe75c1c50..c42d5296a 100644 --- a/roles/aws/aws_iam_role/tasks/main.yml +++ b/roles/aws/aws_iam_role/tasks/main.yml @@ -1,4 +1,4 @@ -- name: Create an IAM Managed Policy if defined. +- name: Create an inline IAM Managed Policy if defined. amazon.aws.iam_managed_policy: policy_name: "inline_{{ aws_iam_role.inline_policies.name }}_policy" policy: @@ -23,19 +23,19 @@ - name: Create list of strings for predefined policies. ansible.builtin.set_fact: - allowed_strings: ["ec2", "ecs", "backup"] + allowed_strings: ["ec2", "ecs", "backup", "event"] - name: Create assume role policy document if predefined string is passed. ansible.builtin.set_fact: _assume_role_policy: "{{ lookup('file', aws_iam_role.policy_document + '_document_policy.json') }}" - when: aws_iam_role.policy_document in allowed_strings + when: aws_iam_role.policy_document in allowed_strings and aws_iam_role.policy_document != "event" - name: Create assume role policy document if template is provided. ansible.builtin.set_fact: _assume_role_policy: "{{ aws_iam_role.policy_document }}" when: aws_iam_role.policy_document not in allowed_strings -- name: Create an IAM role. +- name: Create an IAM role {{ aws_iam_role.name }}. amazon.aws.iam_role: profile: "{{ aws_iam_role.aws_profile }}" name: "{{ aws_iam_role.name }}" @@ -50,6 +50,7 @@ - name: Wait for 6 seconds for IAM to be ready. ansible.builtin.wait_for: timeout: 6 + when: ansible_loop.last is defined and ansible_loop.last - name: Register aws_iam_role results. ansible.builtin.set_fact: diff --git a/roles/aws/aws_rds/tasks/main.yml b/roles/aws/aws_rds/tasks/main.yml index 602f13439..b75866e33 100644 --- a/roles/aws/aws_rds/tasks/main.yml +++ b/roles/aws/aws_rds/tasks/main.yml @@ -45,7 +45,7 @@ password: "{{ aws_rds.master_user_password }}" db_subnet_group_name: "{{ aws_rds.name }}" vpc_security_group_ids: "{{ _aws_security_group_list }}" - backup_retention_period: "{{ aws_rds.backup_retention_period | default(35) }}" + backup_retention_period: "{{ aws_rds.backup_retention_period | default(1) }}" character_set_name: "{{ aws_rds.character_set_name | default(omit) }}" tags: "{{ aws_rds.tags | combine({'Name': aws_rds.name}) }}" when: "'aurora' in aws_rds.engine" @@ -157,7 +157,7 @@ - "{{ _aws_sns_topic_info.sns_arn }}" when: - _aws_sns_topic_info is defined - - _aws_sns_topic_info.sns_arn + - _aws_sns_topic_info.sns_arn | length > 0 - name: Create RDS CloudWatch alarms. ansible.builtin.include_role: diff --git a/roles/debian/apache/defaults/main.yml b/roles/debian/apache/defaults/main.yml index c76ddc0b0..4b7779eff 100644 --- a/roles/debian/apache/defaults/main.yml +++ b/roles/debian/apache/defaults/main.yml @@ -4,6 +4,8 @@ php: - 8.1 # see https://www.php.net/supported-versions.php symfony_env: "{{ _env_type }}" apache: + # le_email: sysadm@codeenigma.com Globally defined email for Let's Encrypt notifications + # le_cron: Globally defined cron for LE renewals # Global default config for apache2.conf. user: www-data mods_enabled: diff --git a/roles/debian/nginx/defaults/main.yml b/roles/debian/nginx/defaults/main.yml index 86144f452..38fd8bf1c 100644 --- a/roles/debian/nginx/defaults/main.yml +++ b/roles/debian/nginx/defaults/main.yml @@ -8,6 +8,8 @@ symfony_env: "{{ _env_type }}" # Nginx variables actually start here. nginx: # Global default config for nginx.conf. + # le_email: sysadm@codeenigma.com Globally defined email for Let's Encrypt notifications + # le_cron: Globally defined cron for LE renewals user: www-data worker_processes: auto events: @@ -130,9 +132,6 @@ nginx: php_fastcgi_backend: "127.0.0.1:90{{ php.version[-1] | replace('.', '') }}" # for unix socket use "unix:/var/run/php{{ php.version[-1] | replace('.','') }}-fpm.sock" ratelimitingcrawlers: false client_max_body_size: "700M" - ssl_ciphers: [] # Defaults to empty list, you can add ciphers if needed -# - ECDHE-RSA-AES256-GCM-SHA384 -# - ECDHE-RSA-CHACHA20-POLY1305 # drupal_fallback: [] # Default location behavior for nginx # If no custom location behavior is defined, this will be used: diff --git a/roles/debian/ssl/tasks/letsencrypt.yml b/roles/debian/ssl/tasks/letsencrypt.yml index 74350945a..9c3eb69d5 100644 --- a/roles/debian/ssl/tasks/letsencrypt.yml +++ b/roles/debian/ssl/tasks/letsencrypt.yml @@ -35,6 +35,36 @@ when: - ssl.letsencrypt.venv_install_username is defined +- name: Set LE email if defined globally for nginx. + ansible.builtin.set_fact: + _le_email: "{{ nginx.le_email }}" + when: nginx.le_email is defined + +- name: Set LE email if defined globally for apache. + ansible.builtin.set_fact: + _le_email: "{{ apache.le_email }}" + when: apache.le_email is defined + +- name: Set LE email if defined per item. + ansible.builtin.set_fact: + _le_email: "{{ ssl.email }}" + when: ssl.email is defined + +- name: Set LE cron if defined globally for nginx. + ansible.builtin.set_fact: + _le_cron: "{{ nginx.le_cron }}" + when: nginx.le_cron is defined + +- name: Set LE cron if defined globally for apache. + ansible.builtin.set_fact: + _le_cron: "{{ apache.le_cron }}" + when: apache.le_cron is defined + +- name: Set LE cron if defined per item. + ansible.builtin.set_fact: + _le_cron: "{{ ssl.on_calendar }}" + when: ssl.on_calendar is defined + # Install Python applications. - name: Manage required pip packages. ansible.builtin.include_role: @@ -101,13 +131,13 @@ when: not _letsencrypt_cert.stat.exists - name: Register certificate bypassing web server if needed - standalone. - ansible.builtin.command: "{{ _venv_path }}/bin/certbot {{ ssl.certbot_register_command }} --cert-name {{ ssl_facts[_ssl_domains[0]].domain }} --http-01-port {{ ssl.http_01_port }} -m {{ ssl.email }} --{{ _ssl_web_server }}{{ _letsencrypt_domain_string }}" + ansible.builtin.command: "{{ _venv_path }}/bin/certbot {{ ssl.certbot_register_command }} --cert-name {{ ssl_facts[_ssl_domains[0]].domain }} --http-01-port {{ ssl.http_01_port }} -m {{ _le_email }} --{{ _ssl_web_server }}{{ _letsencrypt_domain_string }}" when: - not _letsencrypt_cert.stat.exists - ssl.web_server == "standalone" - name: Register certificate bypassing web server if needed - webroot. - ansible.builtin.command: "{{ _venv_path }}/bin/certbot certonly --webroot -w {{ domain.webroot }} --cert-name {{ ssl_facts[_ssl_domains[0]].domain }} {{ _letsencrypt_domain_string }}" + ansible.builtin.command: "{{ _venv_path }}/bin/certbot certonly --webroot -w /tmp --cert-name {{ ssl_facts[_ssl_domains[0]].domain }} {{ _letsencrypt_domain_string }} -m {{ _le_email }} --non-interactive" when: - not _letsencrypt_cert.stat.exists - ssl.web_server == "webroot" @@ -134,7 +164,11 @@ - name: Build timer variables with dynamic key. ansible.builtin.set_fact: - _certbot_renewal_timer: "{'certbot_renewal':{'timer_command':'/usr/local/bin/le_cron.sh','timer_OnCalendar':'{{ ssl.on_calendar }}','timer_persistent':'true'}}" + _certbot_renewal_timer: + certbot_renewal: + timer_command: "/usr/local/bin/le_cron.sh" + timer_OnCalendar: "{{ _le_cron }}" + timer_persistent: true when: ssl.autorenew - name: Create systemd timer for certificate renewal. diff --git a/roles/debian/ssl/templates/le_cron.sh.j2 b/roles/debian/ssl/templates/le_cron.sh.j2 index 31148628a..404524aad 100644 --- a/roles/debian/ssl/templates/le_cron.sh.j2 +++ b/roles/debian/ssl/templates/le_cron.sh.j2 @@ -35,7 +35,7 @@ certbot_exit_code=$? if [ $certbot_exit_code -ne 0 ]; then # Certbot failed, send email notification - recipient="{{ ssl.email }}" + recipient="{{ _le_email }}" subject="Certbot Renewal Failed" hostname=$(cat /etc/hostname) body="Certbot renewal failed on server $hostname with the following output: