DavidWells / markdown-magic

💫  Automatically format markdown files via comment blocks using source code, external data & custom transforms.
811 stars 227 forks source link

AWS Conditional HostedZone #68

Closed DavidWells closed 5 months ago

DavidWells commented 7 months ago

See Condition: CreateZone


service: netlify-ga-proxy

plugins:
  - serverless-pseudo-parameters
  - serverless-manifest-plugin

custom:
  # default stage 'prod'
  stage: ${opt:stage, 'prod'}
  # default region 'us-west-1'
  region: ${opt:region, 'us-west-1'}
  # Primary host domain
  baseDomain: cognitoguide.com
  # api.netlify-services.com/users/
  domainsByStage:
    prod:    api.${self:custom.baseDomain}
    staging: api-staging.${self:custom.baseDomain}
    dev:     api-dev.${self:custom.baseDomain}
  # Resolved domain settings
  apiDomain: ${self:custom.domainsByStage.${self:custom.stage}}
  # Base mapping. domain.com/${apiBasePath}/
  apiBasePath: 'mypath'
  # Resolved full service url
  apiServiceUrl: 'https://${self:custom.apiDomain}/${self:custom.apiBasePath}'
  # Route53 hosted zone
  hostedZoneId: Z10434111YXXDAXXD5TT0

provider:
  name: aws
  stage: ${self:custom.stage}
  region: ${self:custom.region}
  runtime: nodejs12.x
  httpApi:
    cors: true

functions:
  proxy:
    handler: handler.proxy
    events:
      - httpApi:
          method: GET
          path: /r/collect
      - httpApi:
          method: GET
          path: /collect

resources:
  Conditions:
    ## Create HostedZone if hostedZoneId empty
    CreateZone: { "Fn::Equals" : ["${self:custom.hostedZoneId, ''}", ""] }
  Resources:
    ## Hack for conditional depends clause via https://bit.ly/2X0beJL
    WaitOnHostedZone:
      Condition: CreateZone
      DependsOn: HostedZone
      Type: "AWS::CloudFormation::WaitConditionHandle"
    EmptyWait:
      Type: "AWS::CloudFormation::WaitConditionHandle"
    CustomWaitCondition:
      Type: "AWS::CloudFormation::WaitCondition"
      Properties:
        Handle: { "Fn::If": [CreateZone, { Ref: WaitOnHostedZone }, { Ref: EmptyWait }] }
        Timeout: "1"
        Count: 0
    ## https://amzn.to/3d0ZBaU
    HostedZone:
      Type: 'AWS::Route53::HostedZone'
      Condition: CreateZone
      Properties:
        HostedZoneConfig:
          Comment: 'Hosted zone for ${self:custom.baseDomain}'
        Name: '${self:custom.baseDomain}'
    Domain:
      Type: 'AWS::ApiGatewayV2::DomainName'
      DependsOn:
        - SSLCertificate
      Properties:
        DomainName: ${self:custom.apiDomain}
        DomainNameConfigurations:
          - EndpointType: REGIONAL
            CertificateArn: { Ref: SSLCertificate }
    ApiMapping:
      Type: 'AWS::ApiGatewayV2::ApiMapping'
      DependsOn:
        - HttpApi
        - HttpApiStage
        - Domain
      Properties:
        DomainName: ${self:custom.apiDomain}
        ApiId: { Ref: HttpApi }
        # What is the route
        ApiMappingKey: ${self:custom.apiBasePath}
        Stage: { Ref: HttpApiStage }
    DnsRecord:
      Type: AWS::Route53::RecordSetGroup
      Properties:
        HostedZoneName: '${self:custom.baseDomain}.'
        RecordSets:
          - Name: ${self:custom.apiDomain}
            Type: A
            AliasTarget:
              HostedZoneId: { Fn::GetAtt: [ Domain, RegionalHostedZoneId ] }
              DNSName: { Fn::GetAtt: [ Domain, RegionalDomainName ] }
    SSLCertificate:
      Type: 'Custom::DNSCertificate'
      DependsOn:
        - CustomWaitCondition
        - CustomAcmCertificateLambdaExecutionRole
        - CustomAcmCertificateLambda
      Properties:
        DomainName: '${self:custom.baseDomain}'
        SubjectAlternativeNames:
          - '*.${self:custom.baseDomain}'
        ValidationMethod: DNS
        Region: ${self:custom.region}
        DomainValidationOptions:
          - DomainName: '${self:custom.baseDomain}'
            HostedZoneId: '${self:custom.hostedZoneId}' # '#{HostedZone}'
        ServiceToken: { Fn::GetAtt: [ CustomAcmCertificateLambda, Arn ] }
    CustomAcmCertificateLambdaExecutionRole:
      Type: 'AWS::IAM::Role'
      Properties:
        AssumeRolePolicyDocument:
          Statement:
            - Action:
                - sts:AssumeRole
              Effect: Allow
              Principal:
                Service: lambda.amazonaws.com
          Version: '2012-10-17'
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
          - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
        Policies:
          - PolicyDocument:
              Statement:
                - Action:
                    - acm:AddTagsToCertificate
                    - acm:DeleteCertificate
                    - acm:DescribeCertificate
                    - acm:RemoveTagsFromCertificate
                  Effect: Allow
                  Resource:
                    - 'arn:aws:acm:*:${AWS::AccountId}:certificate/*'
                - Action:
                    - acm:RequestCertificate
                    - acm:ListTagsForCertificate
                    - acm:ListCertificates
                  Effect: Allow
                  Resource:
                    - '*'
                - Action:
                    - route53:ChangeResourceRecordSets
                  Effect: Allow
                  Resource:
                    - arn:aws:route53:::hostedzone/*
              Version: '2012-10-17'
            PolicyName: 'CustomAcmCertificateLambdaExecutionPolicy-${self:service}'
    CustomAcmCertificateLambda:
      Type: 'AWS::Lambda::Function'
      Metadata:
        Source: https://github.com/dflook/cloudformation-dns-certificate
        Version: 1.7.2
      Properties:
        Description: Cloudformation custom resource for DNS validated certificates
        Handler: index.handler
        Role: { Fn::GetAtt: [ CustomAcmCertificateLambdaExecutionRole, Arn ] }
        Runtime: python3.6
        Timeout: 900
        Code:
          ZipFile: "y=Exception\nU=RuntimeError\nM=True\nimport copy,hashlib as t,json,logging\
            \ as B,time\nfrom boto3 import client as J\nfrom botocore.exceptions import\
            \ ClientError as u,ParamValidationError as v\nfrom urllib.request import\
            \ Request as w,urlopen as x\nA=B.getLogger()\nA.setLevel(B.INFO)\nC=A.info\n\
            R=A.exception\nL=copy.copy\nS=time.sleep\nT=lambda j:json.dumps(j,sort_keys=M).encode()\n\
            def handler(e,c):\n\tAB='OldResourceProperties';AA='Update';A9='Delete';A8='None';A7='acm';A6='FAILED';A5='properties';A4='stack-id';A3='logical-id';A2='DNS';s='Old';r='Certificate';q='LogicalResourceId';p='DomainName';o='ValidationMethod';n='Route53RoleArn';m='Region';d='RequestType';b='Reinvoked';a='StackId';Z=None;Q='Status';P='Key';O='';N='DomainValidationOptions';K=False;I='ResourceProperties';H='cloudformation:';G='Value';F='CertificateArn';E='Tags';B='PhysicalResourceId';f=c.get_remaining_time_in_millis;C(e)\n\
            \tdef g():\n\t\tC=L(A)\n\t\tfor G in ['ServiceToken',m,E,n]:C.pop(G,Z)\n\
            \t\tif o in A:\n\t\t\tif A[o]==A2:\n\t\t\t\tfor H in set([A[p]]+A.get('SubjectAlternativeNames',[])):k(H)\n\
            \t\t\t\tdel C[N]\n\t\te[B]=D.request_certificate(IdempotencyToken=A0,**C)[F];l()\n\
            \tdef V(a):\n\t\twhile M:\n\t\t\ttry:D.delete_certificate(**{F:a});return\n\
            \t\t\texcept u as B:\n\t\t\t\tR(O);A=B.response['Error']['Code']\n\t\t\t\
            \tif A=='ResourceInUseException':\n\t\t\t\t\tif f()/1000<30:raise\n\t\t\t\
            \t\tS(5);continue\n\t\t\t\tif A in['ResourceNotFoundException','ValidationException']:return\n\
            \t\t\t\traise\n\t\t\texcept v:return\n\tdef W(p):\n\t\tfor I in D.get_paginator('list_certificates').paginate():\n\
            \t\t\tfor A in I['CertificateSummaryList']:\n\t\t\t\tC(A);B={B[P]:B[G]for\
            \ B in D.list_tags_for_certificate(**{F:A[F]})[E]}\n\t\t\t\tif B.get(H+A3)==e[q]and\
            \ B.get(H+A4)==e[a]and B.get(H+A5)==X(p):return A[F]\n\tdef h():\n\t\tif\
            \ e.get(b,K):raise U('Certificate not issued in time')\n\t\te[b]=M;C(e);J('lambda').invoke(FunctionName=c.invoked_function_arn,InvocationType='Event',Payload=T(e))\n\
            \tdef i():\n\t\twhile f()/1000>30:\n\t\t\tA=D.describe_certificate(**{F:e[B]})[r];C(A)\n\
            \t\t\tif A[Q]=='ISSUED':return M\n\t\t\telif A[Q]==A6:raise U(A.get('FailureReason',O))\n\
            \t\t\tS(5)\n\t\treturn K\n\tdef z():A=L(e[s+I]);A.pop(E,Z);B=L(e[I]);B.pop(E,Z);return\
            \ A!=B\n\tdef j():\n\t\tW='Type';V='Name';U='HostedZoneId';T='ValidationStatus';R='PENDING_VALIDATION';K='ResourceRecord'\n\
            \t\tif A.get(o)!=A2:return\n\t\twhile M:\n\t\t\tH=D.describe_certificate(**{F:e[B]})[r];C(H)\n\
            \t\t\tif H[Q]!=R:return\n\t\t\tif not[A for A in H.get(N,[{}])if T not in\
            \ A or K not in A]:break\n\t\t\tS(1)\n\t\tfor E in H[N]:\n\t\t\tif E[T]==R:L=k(E[p]);O=L.get(n,A.get(n));I=J('sts').assume_role(RoleArn=O,RoleSessionName=(r+e[q])[:64],DurationSeconds=900)['Credentials']if\
            \ O is not Z else{};P=J('route53',aws_access_key_id=I.get('AccessKeyId'),aws_secret_access_key=I.get('SecretAccessKey'),aws_session_token=I.get('SessionToken')).change_resource_record_sets(**{U:L[U],'ChangeBatch':{'Comment':'Domain\
            \ validation for '+e[B],'Changes':[{'Action':'UPSERT','ResourceRecordSet':{V:E[K][V],W:E[K][W],'TTL':60,'ResourceRecords':[{G:E[K][G]}]}}]}});C(P)\n\
            \tdef k(n):\n\t\tC='.';n=n.rstrip(C);D={B[p].rstrip(C):B for B in A[N]};B=n.split(C)\n\
            \t\twhile len(B):\n\t\t\tif C.join(B)in D:return D[C.join(B)]\n\t\t\tB=B[1:]\n\
            \t\traise U(N+' missing for '+n)\n\tX=lambda v:t.new('md5',T(v)).hexdigest()\n\
            \tdef l():A=L(e[I].get(E,[]));A+=[{P:H+A3,G:e[q]},{P:H+A4,G:e[a]},{P:H+'stack-name',G:e[a].split('/')[1]},{P:H+A5,G:X(e[I])}];D.add_tags_to_certificate(**{F:e[B],E:A})\n\
            \tdef Y():\n\t\tC(e);A=x(w(e['ResponseURL'],T(e),{'content-type':O},method='PUT'))\n\
            \t\tif A.status!=200:raise y(A)\n\ttry:\n\t\tA0=X(e['RequestId']+e[a]);A=e[I];D=J(A7,region_name=A.get(m));e[Q]='SUCCESS'\n\
            \t\tif e[d]=='Create':\n\t\t\tif e.get(b,K)is K:e[B]=A8;g()\n\t\t\tj()\n\
            \t\t\tif not i():return h()\n\t\telif e[d]==A9:\n\t\t\tif e[B]!=A8:\n\t\t\
            \t\tif e[B].startswith('arn:'):V(e[B])\n\t\t\t\telse:V(W(A))\n\t\telif e[d]==AA:\n\
            \t\t\tif z():\n\t\t\t\tC(AA)\n\t\t\t\tif W(A)==e[B]:\n\t\t\t\t\ttry:D=J(A7,region_name=e[AB].get(m));C(A9);V(W(e[AB]))\n\
            \t\t\t\t\texcept:R(O)\n\t\t\t\t\treturn Y()\n\t\t\t\tif e.get(b,K)is K:g()\n\
            \t\t\t\tj()\n\t\t\t\tif not i():return h()\n\t\t\telse:\n\t\t\t\tif E in\
            \ e[s+I]:D.remove_tags_from_certificate(**{F:e[B],E:e[s+I][E]})\n\t\t\t\t\
            l()\n\t\telse:raise U(e[d])\n\t\treturn Y()\n\texcept y as A1:R(O);e[Q]=A6;e['Reason']=str(A1);return\
            \ Y()"
  Outputs:
    DomainName:
      Description: API Gateway custom domain
      Value: { Ref: Domain }
    ServiceUrl:
      Description: Custom API Gateway service URL
      Value: ${self:custom.apiServiceUrl}
    DomainMapping:
      Description: Apigateway mapping
      Value: { Ref: ApiMapping }
    CertificateArn:
      Description: ARN of custom domain cert
      Value: { Ref: SSLCertificate }