serverless-website.yaml
---

UA CloudOPS Serverless Website Template

This CloudFormation template will deploy the following resources:

  • S3 bucket configured to host static web content
  • IAM user with access to the bucket
  • Optional CloudFront Distribution
AWSTemplateFormatVersion: '2010-09-09' Description: UA CloudOPS Serverless Website

Parameters

These are the input parameters for this template. All of these parameters must be supplied for this template to be deployed.

Parameters: SiteName: Type: String Description: The DNS name of the website (no spaces). This will be the bucket name also. AllowedPattern: "(?!-)[a-zA-Z0-9-.]{1,63}(?<!-)" ConstraintDescription: Must be a valid character in a DNS name. CertificateARN: Type: String Description: The ARN of the AWS Certificate Manager certificate to use. (Only required for HTTPS Websites)

Tags

No tags are explicitly set on the resources. Tags must be set as part of the template deployment with the --tags option during stack creation. https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-stack.html Tags for auditing/accounting

TagContactNetid: Description: NetID of contact for resource Type: String TagCloudOpsGroup: Description: CloudOps Group for which resource was created Type: String TagAccountNumber: Description: KFS account number for resource Type: String TagTicketNumber: Description: Jira ticket associated with the stack and its resources Type: String TagCloudOpsGroupId: Type: String Description: Identifies the owning CloudOps group ID.

Metadata

Metadata is mostly for organizing and presenting Parameters in a better way when using CloudFormation in the AWS Web UI.

Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Website Configuration Parameters: - SiteName - CertificateARN ParameterLabels: SiteName: default: "Site Name" CertificateARN: default: 'SSL Certificate'

Mappings

Mappings:

!FindInMap [AccountMap, !Ref "AWS::AccountId", certificate]

AccountMap:

CloudOps NonProd

"237838427284": "certificate": "arn:aws:acm:us-east-1:237838427284:certificate/88c91585-fcd9-44cc-97a9-80422051d40c" "hostedzone": "cloudsites-nonprod.arizona.edu" "bucketsuffix": "cloudsites-nonprod"

CloudOps Prod

"445481493426": "certificate": "arn:aws:acm:us-east-1:445481493426:certificate/29a33d2e-9dfc-41e9-a28f-a8316862a866" "hostedzone": "cloudsites.arizona.edu" "bucketsuffix": "cloudsites"

Conditions

Determine if we're using the default wildcard certificate, or if this is a custom cert.

Conditions: IsWildcardCert: !Equals [!Ref CertificateARN, !FindInMap [AccountMap, !Ref "AWS::AccountId", certificate]] IsCustomCert: !Not [ !Equals [!Ref CertificateARN, !FindInMap [AccountMap, !Ref "AWS::AccountId", certificate]] ] Resources: S3WebsiteBucket: Type: AWS::S3::Bucket Properties: BucketName: Fn::Sub: - "${AWS::StackName}.${suffix}" - suffix: !FindInMap [AccountMap, !Ref "AWS::AccountId", bucketsuffix] AccessControl: PublicRead WebsiteConfiguration: IndexDocument: index.html ErrorDocument: index.html Tags: - Key: contactnetid Value: !Ref TagContactNetid - Key: cloudopsgroup Value: !Ref TagCloudOpsGroup - Key: accountnumber Value: !Ref TagAccountNumber - Key: ticketnumber Value: !Ref TagTicketNumber - Key: service Value: "CloudOps Serverless Website" - Key: groupid Value: !Ref TagCloudOpsGroupId

S3 Bucket Policy

Sets the required policy on the S3 bucket to allow web hosting.

S3BucketPublicPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: S3WebsiteBucket PolicyDocument: Statement: - Action: - s3:GetObject Effect: Allow Resource: !Sub "arn:aws:s3:::${S3WebsiteBucket}/*" Principal: "*"

S3 Bucket User

Creates an IAM user that can only connect to the S3 bucket specified.

S3BucketUser: Type: AWS::IAM::User Properties: Path: "/" Policies: - PolicyName: s3BucketAccess PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - s3:ListAllMyBuckets Resource: - "*" - Effect: Allow Action: - s3:* Resource: !Sub "arn:aws:s3:::${S3WebsiteBucket}*" CloudFrontDistribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Aliases: - !Ref SiteName Origins: - DomainName: !Sub "${S3WebsiteBucket}.s3.amazonaws.com" Id: !Sub "S3-${S3WebsiteBucket}" S3OriginConfig: OriginAccessIdentity: "" Enabled: true Comment: !Sub "CloudFront Distribution for ${SiteName}" CustomErrorResponses: - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: "/" DefaultRootObject: index.html DefaultCacheBehavior: TargetOriginId: !Sub "S3-${S3WebsiteBucket}" ForwardedValues: QueryString: false Cookies: Forward: all ViewerProtocolPolicy: redirect-to-https HttpVersion: http2 PriceClass: PriceClass_100 ViewerCertificate: AcmCertificateArn: !Ref CertificateARN MinimumProtocolVersion: TLSv1.2_2018 SslSupportMethod: sni-only Tags: - Key: contactnetid Value: !Ref TagContactNetid - Key: cloudopsgroup Value: !Ref TagCloudOpsGroup - Key: accountnumber Value: !Ref TagAccountNumber - Key: ticketnumber Value: !Ref TagTicketNumber - Key: service Value: "CloudOps Serverless Website" - Key: groupid Value: !Ref TagCloudOpsGroupId

For the website DNS record we're making in Route 53, we need to do something different if we're using the default wildcard SSL cert, or if we're using a custom domain cert. If its the default wildcard cert, then the SiteName must be a subdomain of the local hostedzone, so we can just use the SiteName that is passed in. If its a custom domain, then the route53 record we make still needs to be a subdomain of the local hostedzone, so we have to build one from the stack name. This will become the CNAME target for the customer to reference wherever they are setting their custom domain up at.

WebsiteDnsRecord: Type: AWS::Route53::RecordSet Properties: HostedZoneName: Fn::Sub: - "${hostedzone}." - hostedzone: !FindInMap [AccountMap, !Ref "AWS::AccountId", hostedzone] Name: !If - IsWildcardCert - !Sub "${SiteName}." - Fn::Sub: - "${AWS::StackName}.${hostedzone}." - hostedzone: !FindInMap [AccountMap, !Ref "AWS::AccountId", hostedzone] Type: CNAME TTL: '900' ResourceRecords: - !GetAtt CloudFrontDistribution.DomainName AccountProperties: Type: Custom::AccountProperties Properties: ServiceToken: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:fdn-cf-account-info" SSMParameters: - /cloudops/serverless-website/template-bucket DefaultTemplate: Type: Custom::DefaultTemplate Properties: ServiceToken: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:copy-default-serverless-site" TargetBucketName: !Ref S3WebsiteBucket SourceBucketName: !GetAtt AccountProperties.cloudops_serverless-website_template-bucket Outputs: WebsiteURL: Value: !Sub "https://${SiteName}/" WebsiteDnsRecord: Value: !Ref WebsiteDnsRecord BucketName: Value: !Ref S3WebsiteBucket BucketUser: Value: !Ref S3BucketUser CloudFrontDistribution: Value: !Ref CloudFrontDistribution CloudFrontARN: Value: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}" CloudFrontDNS: Value: !GetAtt CloudFrontDistribution.DomainName CloudOpsGroupId: Value: !Ref TagCloudOpsGroupId CustomDomain: Value: !If [IsCustomCert, "true", "false"]