import json import re from datetime import datetime, timezone from typing import Any, Optional import boto3 from flask import current_app from app.extensions import db from app.models.mirrors import Proxy from app.terraform.proxy import ProxyAutomation, update_smart_proxy_instance class ProxyCloudfrontAutomation(ProxyAutomation): short_name = "proxy_cloudfront" description = "Deploy proxies to AWS CloudFront" provider = "cloudfront" smart_proxies = True cloud_name = "aws" template_parameters = [ "admin_email", "aws_access_key", "aws_secret_key", "smart_zone", ] template = """ terraform { {{ backend_config }} required_providers { acme = { source = "vancluever/acme" version = "~> 2.11.0" } aws = { version = "~> 4.41.0" } } } provider "acme" { server_url = "https://acme-v02.api.letsencrypt.org/directory" } provider "aws" { access_key = "{{ aws_access_key }}" secret_key = "{{ aws_secret_key }}" region = "us-east-2" } locals { smart_zone = "{{ smart_zone }}" } {% for group in groups %} module "label_{{ group.id }}" { source = "cloudposse/label/null" version = "0.25.0" namespace = "{{ global_namespace }}" tenant = "{{ group.group_name }}" label_order = ["namespace", "tenant", "name", "attributes"] } module "log_bucket_{{ group.id }}" { source = "cloudposse/s3-log-storage/aws" version = "0.28.0" context = module.label_{{ group.id }}.context name = "logs" attributes = ["cloudfront"] acl = "log-delivery-write" standard_transition_days = 30 glacier_transition_days = 60 expiration_days = 90 } resource "aws_sns_topic" "alarms_{{ group.id }}" { name = "${module.label_{{ group.id }}.id}-cloudfront-alarms" } {% for origin in group.origins | selectattr("destroyed", "none") | selectattr("smart") %} {% if loop.first %} module "smart_proxy_{{ group.id }}" { source = "{{ terraform_modules_path }}/terraform-aws-bc-smart-proxy-instance" context = module.label_{{ group.id }}.context name = "smart-proxy" config_filename = "smart_proxy.{{ group.id }}.conf" disable_api_termination = false dns_zone = "{{ smart_zone }}" letsencrypt_email_address = "{{ admin_email }}" max_transfer_per_hour = "13000000000" } {% endif %} {% endfor %} {% endfor %} {% for proxy in proxies %} module "cloudfront_{{ proxy.id }}" { source = "{{ terraform_modules_path }}/terraform-aws-bc-proxy" {% if proxy.origin.smart %} origin_domain = "origin-{{ proxy.origin.id }}.{{ proxy.origin.group.group_name }}.smart.{{ smart_zone[:-1] }}" {% else %} origin_domain = "{{ proxy.origin.domain_name }}" {% endif %} logging_bucket = module.log_bucket_{{ proxy.origin.group.id }}.bucket_domain_name sns_topic_arn = aws_sns_topic.alarms_{{ proxy.origin.group.id }}.arn low_bandwidth_alarm = false context = module.label_{{ proxy.origin.group.id }}.context name = "proxy" attributes = ["{{ proxy.origin.domain_name }}"] bypass_token = "{{ bypass_token }}" } {% endfor %} """ def tf_posthook(self, *, prehook_result: Any = None, logs: Optional[str] = None) -> None: self.import_state(self.tf_show()) failed_ids = [] for line in logs.strip().split('\n'): try: log_entry = json.loads(line) if log_entry.get("@level") == "error" and "CloudFront Distribution" in log_entry.get("@message", ""): match = re.search(r'CloudFront Distribution (\w+) cannot be deleted', log_entry["@message"]) if match: failed_ids.append(match.group(1)) except json.JSONDecodeError: continue client = boto3.client( 'cloudfront', aws_access_key_id=current_app.config["AWS_ACCESS_KEY"], aws_secret_access_key=current_app.config["AWS_SECRET_KEY"], region_name="us-east-1" ) for failed_id in failed_ids: response = client.get_distribution_config(Id=failed_id) etag = response['ETag'] client.delete_distribution(Id=failed_id, IfMatch=etag) def import_state(self, state: Any) -> None: if not isinstance(state, dict): raise RuntimeError("The Terraform state object returned was not a dict.") if "child_modules" not in state["values"]["root_module"]: # There are no CloudFront proxies deployed to import state for return # CloudFront distributions (proxies) for mod in state["values"]["root_module"]["child_modules"]: if mod["address"].startswith("module.cloudfront_"): for res in mod["resources"]: if res["address"].endswith("aws_cloudfront_distribution.this"): proxy = Proxy.query.filter( Proxy.id == mod["address"][len("module.cloudfront_") :] ).first() proxy.url = "https://" + res["values"]["domain_name"] proxy.slug = res["values"]["id"] proxy.terraform_updated = datetime.now(tz=timezone.utc) break # EC2 instances (smart proxies) for g in state["values"]["root_module"]["child_modules"]: if g["address"].startswith("module.smart_proxy_"): group_id = int(g["address"][len("module.smart_proxy_") :]) for s in g["child_modules"]: if s["address"].endswith(".module.instance"): for x in s["resources"]: if x["address"].endswith( ".module.instance.aws_instance.default[0]" ): update_smart_proxy_instance( group_id, self.provider, "us-east-2a", x["values"]["id"], ) db.session.commit()