AWSTemplateFormatVersion: 2010-09-09

Parameters:
  # Must specify parameters by passing `--parameters` flag into cloudformation
  # e.g.
  # ... --parameters ParameterKey=DomainName,ParameterValue="example.com"
  DomainName:
    Type: String
    Description: '(sub)Domain name e.g. (foo.)example.com'

  CertificateArn:
    Type: String
    Description: 'Certificate ARN (must be in `us-east-1`)'

  S3BucketName:
    Type: String
    Description: 'S3 Bucket name'

  CreateRoute53:
    Type: String
    Default: 'false'
    AllowedValues:
      - 'true'
      - 'false'
    Description: 'Indicates whether to create DNS entry or not'

  LogCloudfront:
    Type: String
    Default: 'false'
    AllowedValues:
      - 'true'
      - 'false'
    Description: 'Creates logging for cloufront if true'

Mappings:
  RegionMap:
    # see https://docs.aws.amazon.com/general/latest/gr/rande.html#cf_region
    # there is only one endpoint
    'us-east-1':
      ZoneId: Z2FDTNDATAQYW2

Conditions:
  CreateRoute53Cond: !Equals [ !Ref CreateRoute53, 'true' ]
  LogCloudfrontCond: !Equals [ !Ref LogCloudfront, 'true' ]

Resources:
  S3StorageBucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: Private
      BucketName: !Ref S3BucketName
      MetricsConfigurations:
        - Id: EntireBucket
      VersioningConfiguration:
        Status: Suspended
    # Delete S3 bucket on stack deletion.
    # Bucket must be empty, otherwise deletion will fail
    DeletionPolicy: Delete

  CFIdentityAccess:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Ref DomainName

  S3StorageBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3BucketName
      PolicyDocument:
        Statement:
          - Action:
              - "s3:GetObject"
            Effect: "Allow"
            Resource: !Join ['', ['arn:aws:s3:::', !Ref S3BucketName, '/*']]
            Principal:
              CanonicalUser: !GetAtt CFIdentityAccess.S3CanonicalUserId
            Sid: "Grant CloudFront Origin Identity to serve content"
          # Prevent 403 error codes for missing content
          # ...or can replace CustomErrorResponses.ErrorCode with 403
          - Action:
              - "s3:ListBucket"
            Effect: "Allow"
            Resource: !Join ['', ['arn:aws:s3:::', !Ref S3BucketName]]
            Principal:
              CanonicalUser: !GetAtt CFIdentityAccess.S3CanonicalUserId
            Sid: "Grant CloudFront Origin Identity to list bucket"

  S3LogBucket:
    Type: AWS::S3::Bucket
    Condition: LogCloudfrontCond
    Properties:
      AccessControl: Private
      BucketName: !Join ['-', [!Ref S3BucketName, 'accesslog']]
      VersioningConfiguration:
        Status: Suspended
    # Delete S3 bucket on stack deletion
    # Bucket must be empty, otherwise deletion will fail
    DeletionPolicy: Delete

  CloudfrontCDN:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        IPV6Enabled: true
      Tags:
        - Key: 'website'
          Value:
            Ref: DomainName
      DistributionConfig:
        Origins:
          - DomainName: !GetAtt S3StorageBucket.DomainName
            Id: CdnOrigin
            # Make S3 bucket only accessible through CF
            S3OriginConfig:
              OriginAccessIdentity: !Join ['/', ['origin-access-identity/cloudfront', !Ref CFIdentityAccess]]
        CustomErrorResponses:
          # Default 404 page
          - ErrorCode: 404
            ResponseCode: 404
            ResponsePagePath: '/404'
        # Default root item filename in s3
        DefaultRootObject: 'index'
        DefaultCacheBehavior:
          ViewerProtocolPolicy: redirect-to-https
          TargetOriginId: CdnOrigin
          ForwardedValues:
            QueryString : true
            Cookies:
              Forward : none
        Enabled: true
        # alternate domain CNAMEs
        Aliases:
          - !Ref DomainName
          - !Join ['', ['www.', !Ref DomainName]]
        ViewerCertificate:
          AcmCertificateArn: !Ref CertificateArn
          # Options: {`sni-only`, `vip`}. `sni-only` will incur no
          # additional charges
          SslSupportMethod: sni-only
        Logging:
          Fn::If:
            - LogCloudfrontCond
            - Bucket: !GetAtt S3LogBucket.DomainName
            - Ref: AWS::NoValue
        Comment: !Ref DomainName

  R53HostedZone:
    Type: AWS::Route53::HostedZone
    Condition: CreateRoute53Cond
    Properties:
      Name: !Ref DomainName

  R53RecordSet:
    Type: AWS::Route53::RecordSet
    Condition: CreateRoute53Cond
    Properties:
      Type: A
      AliasTarget:
        DNSName: !GetAtt CloudfrontCDN.DomainName
        # There is only one endpoint
        HostedZoneId: !FindInMap [RegionMap, 'us-east-1', ZoneId]
        EvaluateTargetHealth: false
      HostedZoneId: !Ref R53HostedZone
      Comment: 'Redirect to cloudfront'
      Name: !Ref DomainName

Outputs:

  AWSDomainURL:
    Value: !Join ['', ['https://', !GetAtt S3StorageBucket.DomainName]]
    Description: 'S3 Domain URL'

  CloudFrontURL:
    Value: !GetAtt CloudfrontCDN.DomainName
    Description: 'Cloudfront url'