Deploy Static Website on Aws Using Cloudformation Template

A step by step guide to deploy static website on aws s3 and cloudfront using cloudformation as devops infrastructure as code.

AWS provides a very efficient, performant and cheap way to host the website both static and Single page application aka SPA. When I started to create my blogging website, I decided to automate everything, right from infrastructure to deployment. Here I am sharing my experience with automation of static website deployment.

Note

To start with , you have to have a domain registered. Once you buy a domain, you can set the name server of AWS. Once this is set we are ready to go.

Step 1

First , create an account on AWS. Once done , create a user with programmatic access required to run cloudformation. Keep note of aws_access_key_id and aws_secret_access_key

Step 2

Create a hosted zone on AWS Rout53 Service. Once done , you will get a Hosted Zone ID. Note this Hosted Zone ID, this will be needed for cloudformation.(see last column in below screenshot)

Route53 zone

That's all, Now are ready to automate whole infrastructure.

Solution Overview

As a part of this solution we will use the apex domain example.com and subdomain www.example.com.

  • www.example.com will be main domain to serve data.
  • example.com will be redirected to www.example.com

Below are the resources that will be created in AWS :

  • ACM Certificate for www.example.com & example.com (It's free in AWS. Hurray!!)
  • S3 buckets to hold cloudformation template data and website data.
  • S3 static website for serving default content and redirection. e.g. example.com to www.example.com
  • Route entries for www.example.com & example.com
  • Cloudformation as CDN
  • Lambda Edge to add Secure headers via ContentSecurityPolicy header.

See the flow in solution diagrams below

In Figure 2, if users type example.com, we are just redirecting the user to our main website www.example.com. This is achieved by creating a s3 website named example.com(name of the bucket) and addiing the redirection rule there. Now In cloudfront, we are setting exampel.com s3 website as origin. example.com domain is mapped to cloudfront, which in turn forward request to static website origin.

Apex domain flow
Tip

Why we need cloudfront for example.com, why not directly map domain example.com to s3 website which is doing the redirection ?

Well, We can do that too. Only problem is this works well for http and not for https.

e.g.

http://example.com ---> www.example.com - will work https://example.com ---> www.example.com - will not work, actually browser will not be able to reach https://example.com

To make the issue worse, browsers automatically add https to URL.

To solve this issue, we need cloudfront in between, so that we can handle both http and https.

In Figure 3, user make a request to www.example.com, which will first come to cloudfront edge location. If endpoint is cached, then content will be server from edge location else go to the s3 static website origin. Content from origin will be fetched and secure lamda function will get execued to add security headers , mainly ContentSecurityPolicy header, which returned to the calee as well as cached on edge locations.

Sub domain flow

Tip

Why we need static website as origin here, why not choose simple s3 bucket as Origin ?

Well, When we fetch data for endpoint, we are actually fetching the index.html (excluding SPA) and cloudfront only support root index.html. That's why we need s3 website as origin, because we can set the default object in s3 website as index.html for each folder.

e.g.

http://www.example.com/my-post actually means http://www.example.com/my-post/index.html

And S3 static website help us here to server default object as index.html

Cloudformation Template code

List of files:

  • main.yaml
  • buckets.yaml
  • acm-certificates.yaml
  • cloudfront.yaml
  • routes-records.yaml

main.yaml This will main file and entry point. This will create all stacks step by step.

  1AWSTemplateFormatVersion: 2010-09-09
  2Description: ACFS3 - S3 Static site with CF and ACM (uksb-1qnk6ni7b) (version:v0.5)
  3
  4Metadata:
  5  AWS::CloudFormation::Interface:
  6    ParameterGroups:
  7      - Label:
  8          default: Domain
  9        Parameters:
 10          - SubDomain
 11          - DomainName
 12
 13Mappings:
 14  Solution:
 15    Constants:
 16      Version: 'v0.7'
 17
 18Rules:
 19  OnlyUsEast1:
 20    Assertions:
 21      - Assert:
 22          Fn::Equals:
 23            - !Ref AWS::Region
 24            - us-east-1
 25        AssertDescription: |
 26          This template can only be deployed in the us-east-1 region.
 27          This is because the ACM Certificate must be created in us-east-1          
 28
 29Parameters:
 30  SubDomain:
 31    Description: The part of a website address before your DomainName - e.g. www or img
 32    Type: String
 33    Default: www
 34    AllowedPattern: ^[^.]*$
 35  DomainName:
 36    Description: The part of a website address after your SubDomain - e.g. example.com
 37    Type: String
 38  HostedZoneId:
 39    Description: HostedZoneId for the domain e.g. Z23ABC4XYZL05B
 40    Type: String
 41
 42Resources:
 43  BucketStack:
 44    Type: AWS::CloudFormation::Stack
 45    Properties:
 46      TemplateURL: ./buckets.yaml
 47      Parameters:
 48        DomainName: !Ref DomainName
 49        SubDomain: !Ref SubDomain
 50
 51  AcmCertificateStack:
 52    Type: AWS::CloudFormation::Stack
 53    Properties:
 54      TemplateURL: ./acm-certificate.yaml
 55      Parameters:
 56        SubDomain: !Ref SubDomain
 57        DomainName: !Ref DomainName
 58        HostedZoneId: !Ref HostedZoneId
 59
 60  CloudFrontStack:
 61    Type: AWS::CloudFormation::Stack
 62    Properties:
 63      TemplateURL: ./cloudfront.yaml
 64      Parameters:
 65        SubDomainCertificateArn: !GetAtt AcmCertificateStack.Outputs.SubDomainCertificateArn
 66        ApexDomainCertificateArn: !GetAtt AcmCertificateStack.Outputs.ApexDomainCertificateArn
 67        DomainName: !Ref DomainName
 68        SubDomain: !Ref SubDomain
 69        S3BucketLogsName: !GetAtt BucketStack.Outputs.S3BucketLogsName
 70
 71  RouteRecordsStack:
 72    Type: AWS::CloudFormation::Stack
 73    Properties:
 74      TemplateURL: ./routes-records.yaml
 75      Parameters:
 76        DomainName: !Ref DomainName
 77        SubDomain: !Ref SubDomain
 78        CfSubDomainName: !GetAtt CloudFrontStack.Outputs.SubDomainCloudFrontDistribution
 79        CfApexDomainName: !GetAtt CloudFrontStack.Outputs.ApexDomainCloudFrontDistribution
 80
 81Outputs:
 82  SolutionVersion:
 83    Value: !FindInMap [Solution, Constants, Version]
 84  S3BucketLogs:
 85    Description: Logging bucket
 86    Value: !GetAtt BucketStack.Outputs.S3BucketLogs
 87  S3SubDomainBucket:
 88    Description: Website bucket
 89    Value: !GetAtt BucketStack.Outputs.S3SubDomainBucket
 90  S3BucketLogsName:
 91    Description: Logging bucket name
 92    Value: !GetAtt BucketStack.Outputs.S3BucketLogsName
 93  S3SubDomainBucketName:
 94    Description: Website bucket name
 95    Value: !GetAtt BucketStack.Outputs.S3SubDomainBucketName
 96  SubDomainCertificateArn:
 97    Description: Issued certificate
 98    Value: !GetAtt AcmCertificateStack.Outputs.SubDomainCertificateArn
 99  ApexDomainCertificateArn:
100    Description: Issued certificate
101    Value: !GetAtt AcmCertificateStack.Outputs.ApexDomainCertificateArn
102  CFSubDomainDistributionName:
103    Description: CloudFront distribution
104    Value: !GetAtt CloudFrontStack.Outputs.SubDomainCloudFrontDistribution
105  CFApexDomainDistributionName:
106    Description: CloudFront distribution
107    Value: !GetAtt CloudFrontStack.Outputs.ApexDomainCloudFrontDistribution
108  CloudFrontDomainName:
109    Description: Website address
110    Value: !GetAtt CloudFrontStack.Outputs.CloudFrontDomainName

buckets.yaml This will create bucket example.com and www.example.com, create static website and also create Logs bucket.

 1AWSTemplateFormatVersion: '2010-09-09'
 2Description: ACFS3 - Cert Provider with DNS validation
 3Transform: AWS::Serverless-2016-10-31
 4
 5Parameters:
 6  DomainName:
 7    Type: String
 8  SubDomain:
 9    Type: String
10
11Resources:
12  S3BucketLogs:
13    Type: AWS::S3::Bucket
14    DeletionPolicy: Retain
15    Properties:
16      AccessControl: LogDeliveryWrite
17      BucketEncryption:
18        ServerSideEncryptionConfiguration:
19          - ServerSideEncryptionByDefault:
20              SSEAlgorithm: AES256
21      Tags:
22        - Key: Solution
23          Value: ACFS3
24
25  S3SubDomainBucket:
26    Type: AWS::S3::Bucket
27    DeletionPolicy: Retain
28    Properties:
29      BucketName: !Sub '${SubDomain}.${DomainName}'
30      AccessControl: PublicRead
31      LoggingConfiguration:
32        DestinationBucketName: !Ref 'S3BucketLogs'
33        LogFilePrefix: 'cdn/'
34      WebsiteConfiguration:
35        ErrorDocument: '404.html'
36        IndexDocument: 'index.html'
37
38  S3DomainBucket:
39    Type: AWS::S3::Bucket
40    DeletionPolicy: Retain
41    Properties:
42      BucketName: !Ref DomainName
43      AccessControl: PublicRead
44      LoggingConfiguration:
45        DestinationBucketName: !Ref 'S3BucketLogs'
46        LogFilePrefix: 'rdb/'
47      WebsiteConfiguration:
48        RedirectAllRequestsTo:
49          HostName: !Sub '${SubDomain}.${DomainName}'
50
51  S3BucketPolicy:
52    Type: AWS::S3::BucketPolicy
53    Properties:
54      Bucket: !Ref S3SubDomainBucket
55      PolicyDocument:
56        Version: '2012-10-17'
57        Statement:
58          - Effect: 'Allow'
59            Action: 's3:GetObject'
60            Principal: '*'
61            Resource: !Sub
62              - '${S3BucketRootArn}/*'
63              - {S3BucketRootArn : !GetAtt S3SubDomainBucket.Arn}
64
65
66Outputs:
67  S3DomainBucket:
68    Description: Website bucket
69    Value: !Ref S3DomainBucket
70  S3DomainBucketName:
71    Description: Website bucket name
72    Value: !GetAtt S3DomainBucket.DomainName
73  S3DomainBucketArn:
74    Description: Website bucket locator
75    Value: !GetAtt S3DomainBucket.Arn
76  S3SubDomainBucket:
77    Description: Website bucket
78    Value: !Ref S3SubDomainBucket
79  S3SubDomainBucketName:
80    Description: Website bucket name
81    Value: !Sub '${SubDomain}.${DomainName}'
82  S3SubDomainBucketArn:
83    Description: Website bucket locator
84    Value: !GetAtt S3SubDomainBucket.Arn
85  S3BucketLogs:
86    Description: Logging bucket
87    Value: !Ref S3BucketLogs
88  S3BucketLogsName:
89    Description: Logging bucket Name
90    Value: !GetAtt S3BucketLogs.DomainName
91

acm-certificate.yaml This will generate the certificates for example.com and www.example.com

 1AWSTemplateFormatVersion: '2010-09-09'
 2Description: ACFS3 - Certificate creation
 3
 4Parameters:
 5  DomainName:
 6    Type: String
 7  SubDomain:
 8    Type: String
 9  HostedZoneId:
10    Type: String
11
12Resources:
13  SubDomainCertificate:
14    Type: AWS::CertificateManager::Certificate
15    Properties: 
16      DomainName: !Sub '${SubDomain}.${DomainName}'
17      SubjectAlternativeNames:
18        - Ref: AWS::NoValue
19      DomainValidationOptions:
20        - DomainName: !Sub '${SubDomain}.${DomainName}'
21          HostedZoneId: !Ref HostedZoneId
22      ValidationMethod: DNS
23
24  ApexDomainCertificate:
25    Type: AWS::CertificateManager::Certificate
26    Properties:
27      DomainName: !Sub '${DomainName}'
28      SubjectAlternativeNames:
29        - Ref: AWS::NoValue
30      DomainValidationOptions:
31        - DomainName: !Sub '${DomainName}'
32          HostedZoneId: !Ref HostedZoneId
33      ValidationMethod: DNS
34
35Outputs:
36  SubDomainCertificateArn:
37    Description: Issued certificate
38    Value: !Ref SubDomainCertificate
39  ApexDomainCertificateArn:
40    Description: Issued certificate
41    Value: !Ref ApexDomainCertificate
42

cloudfront.yaml This stack will deploy the cloudformation, set origin, secure lambda headers. You can modify the ContentSecurityPolicy header as per your need

 1ContentSecurityPolicy:
 2              ContentSecurityPolicy: !Join
 3                - "; "
 4                - - "default-src 'self'"
 5                  - "connect-src 'self' links.services.disqus.com www.google-analytics.com googleads.g.doubleclick.net static.doubleclick.net savjee.report-uri.com c.disquscdn.com disqus.com"
 6                  - "font-src 'self' fonts.gstatic.com"
 7                  - "frame-src 'self' disqus.com c.disquscdn.com www.google.com www.youtube.com accounts.google.com"
 8                  - "img-src 'self' 'unsafe-inline' cdn.viglink.com c.disquscdn.com referrer.disqus.com https://*.disquscdn.com www.google-analytics.com www.gstatic.com ssl.gstatic.com i.ytimg.com i.imgur.com images.gr-assets.com s.gr-assets.com data:"
 9                  - "script-src 'self' 'unsafe-eval' 'unsafe-inline' c.disquscdn.com disqus.com arika-dev.disqus.com https://*.disquscdn.com www.google.com www.google-analytics.com www.gstatic.com apis.google.com https://tagmanager.google.com/ https://www.googletagmanager.com"
10                  - "prefetch-src 'self' 'unsafe-eval' 'unsafe-inline' c.disquscdn.com disqus.com arika-dev.disqus.com https://*.disquscdn.com www.google.com www.google-analytics.com www.gstatic.com apis.google.com https://tagmanager.google.com/ https://www.googletagmanager.com"
11                  - "style-src 'self' 'unsafe-inline' c.disquscdn.com https://*.disquscdn.com fonts.googleapis.com https://fonts.googleapis.com/ https://tagmanager.google.com/ "
12                  - "object-src 'none'"
13              Override: true
  1AWSTemplateFormatVersion: '2010-09-09'
  2Description: ACFS3 - CloudFront with Header Security and site content
  3Transform: 'AWS::Serverless-2016-10-31'
  4
  5Parameters:
  6  SubDomainCertificateArn:
  7    Description: Certificate locater for sub domain
  8    Type: String
  9  ApexDomainCertificateArn:
 10    Description: Certificate locater for apex domain
 11    Type: String
 12  DomainName:
 13    Description: Apex domain
 14    Type: String
 15  SubDomain:
 16    Description: Subdomain
 17    Type: String
 18  S3BucketLogsName:
 19    Description: Logging Bucket
 20    Type: String
 21
 22Resources:
 23  ApexDomainCloudFrontDistribution:
 24    Type: AWS::CloudFront::Distribution
 25    Properties:
 26      DistributionConfig:
 27        Aliases:
 28          - !Sub '${DomainName}'
 29        Comment: 'apex domain to redirect to sub domain'
 30        DefaultCacheBehavior:
 31          Compress: true
 32          DefaultTTL: 86400
 33          ForwardedValues:
 34            QueryString: true
 35          MaxTTL: 31536000
 36          TargetOriginId: !Sub 'S3-${AWS::StackName}-apexdomain'
 37          ViewerProtocolPolicy: 'redirect-to-https'
 38        Enabled: true
 39        HttpVersion: 'http2'
 40        IPV6Enabled: true
 41        Logging:
 42          Bucket: !Ref 'S3BucketLogsName'
 43          IncludeCookies: false
 44          Prefix: 'cdn/'
 45        Origins:
 46          - CustomOriginConfig:
 47              HTTPPort: 80
 48              HTTPSPort: 443
 49              OriginKeepaliveTimeout: 5
 50              OriginProtocolPolicy: 'http-only'
 51              OriginReadTimeout: 30
 52              OriginSSLProtocols:
 53                - TLSv1
 54                - TLSv1.1
 55                - TLSv1.2
 56            DomainName: !Sub '${DomainName}.s3-website.${AWS::Region}.amazonaws.com'
 57            Id: !Sub 'S3-${AWS::StackName}-apexdomain'
 58        PriceClass: 'PriceClass_All'
 59        ViewerCertificate:
 60          AcmCertificateArn: !Ref 'ApexDomainCertificateArn'
 61          MinimumProtocolVersion: 'TLSv1.1_2016'
 62          SslSupportMethod: 'sni-only'
 63
 64  SubDomainCloudFrontDistribution:
 65    Type: AWS::CloudFront::Distribution
 66    Properties:
 67      DistributionConfig:
 68        Aliases:
 69          - !Sub '${SubDomain}.${DomainName}'
 70        Comment: 'sub domain to handle real traffic'
 71        DefaultCacheBehavior:
 72          Compress: true
 73          DefaultTTL: 86400
 74          ForwardedValues:
 75            QueryString: true
 76          MaxTTL: 31536000
 77          TargetOriginId: !Sub 'S3-${AWS::StackName}-subdomain'
 78          ViewerProtocolPolicy: 'redirect-to-https'
 79          ResponseHeadersPolicyId: !Ref ResponseHeadersPolicy
 80        CustomErrorResponses:
 81          - ErrorCachingMinTTL: 60
 82            ErrorCode: 404
 83            ResponseCode: 404
 84            ResponsePagePath: '/404.html'
 85          - ErrorCachingMinTTL: 60
 86            ErrorCode: 403
 87            ResponseCode: 403
 88            ResponsePagePath: '/403.html'
 89        Enabled: true
 90        HttpVersion: 'http2'
 91        DefaultRootObject: 'index.html'
 92        IPV6Enabled: true
 93        Logging:
 94          Bucket: !Ref 'S3BucketLogsName'
 95          IncludeCookies: false
 96          Prefix: 'cdn/'
 97        Origins:
 98          - CustomOriginConfig:
 99              HTTPPort: 80
100              HTTPSPort: 443
101              OriginKeepaliveTimeout: 5
102              OriginProtocolPolicy: 'http-only'
103              OriginReadTimeout: 30
104              OriginSSLProtocols:
105                - TLSv1
106                - TLSv1.1
107                - TLSv1.2
108            DomainName: !Sub '${SubDomain}.${DomainName}.s3-website.${AWS::Region}.amazonaws.com'
109            Id: !Sub 'S3-${AWS::StackName}-subdomain'
110        PriceClass: 'PriceClass_All'
111        ViewerCertificate:
112          AcmCertificateArn: !Ref 'SubDomainCertificateArn'
113          MinimumProtocolVersion: 'TLSv1.1_2016'
114          SslSupportMethod: 'sni-only'
115
116  CloudFrontOriginAccessIdentity:
117    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
118    Properties:
119      CloudFrontOriginAccessIdentityConfig:
120        Comment: !Sub 'CloudFront OAI for ${SubDomain}.${DomainName}'
121        
122  ResponseHeadersPolicy:
123      Type: AWS::CloudFront::ResponseHeadersPolicy
124      Properties: 
125        ResponseHeadersPolicyConfig: 
126          Name: !Sub "${AWS::StackName}-static-site-security-headers"
127          SecurityHeadersConfig: 
128            StrictTransportSecurity: 
129              AccessControlMaxAgeSec: 63072000
130              IncludeSubdomains: true
131              Override: true
132              Preload: true
133            ContentSecurityPolicy:
134              ContentSecurityPolicy: !Join
135                - "; "
136                - - "default-src 'self'"
137                  - "connect-src 'self' links.services.disqus.com www.google-analytics.com googleads.g.doubleclick.net static.doubleclick.net savjee.report-uri.com c.disquscdn.com disqus.com"
138                  - "font-src 'self' fonts.gstatic.com"
139                  - "frame-src 'self' disqus.com c.disquscdn.com www.google.com www.youtube.com accounts.google.com"
140                  - "img-src 'self' 'unsafe-inline' cdn.viglink.com c.disquscdn.com referrer.disqus.com https://*.disquscdn.com www.google-analytics.com www.gstatic.com ssl.gstatic.com i.ytimg.com i.imgur.com images.gr-assets.com s.gr-assets.com data:"
141                  - "script-src 'self' 'unsafe-eval' 'unsafe-inline' c.disquscdn.com disqus.com arika-dev.disqus.com https://*.disquscdn.com www.google.com www.google-analytics.com www.gstatic.com apis.google.com https://tagmanager.google.com/ https://www.googletagmanager.com"
142                  - "prefetch-src 'self' 'unsafe-eval' 'unsafe-inline' c.disquscdn.com disqus.com arika-dev.disqus.com https://*.disquscdn.com www.google.com www.google-analytics.com www.gstatic.com apis.google.com https://tagmanager.google.com/ https://www.googletagmanager.com"
143                  - "style-src 'self' 'unsafe-inline' c.disquscdn.com https://*.disquscdn.com fonts.googleapis.com https://fonts.googleapis.com/ https://tagmanager.google.com/ "
144                  - "object-src 'none'"
145              Override: true
146            ContentTypeOptions: 
147              Override: true
148            FrameOptions:
149              FrameOption: DENY
150              Override: true
151            ReferrerPolicy: 
152              ReferrerPolicy: "same-origin"
153              Override: true
154            XSSProtection: 
155              ModeBlock: true
156              Override: true
157              Protection: true
158
159Outputs:
160  SubDomainCloudFrontDistribution:
161    Description: CloudFront distribution
162    Value: !GetAtt SubDomainCloudFrontDistribution.DomainName
163
164  ApexDomainCloudFrontDistribution:
165    Description: CloudFront distribution
166    Value: !GetAtt ApexDomainCloudFrontDistribution.DomainName
167
168  CloudFrontDomainName:
169    Description: Website address
170    Value: !Sub '${SubDomain}.${DomainName}'
171

routes-records.yaml This will create and verify Route53 Records and point them to cloudfront.

 1AWSTemplateFormatVersion: '2010-09-09'
 2Description: ACFS3 - CloudFront with Header Security and site content
 3Transform: 'AWS::Serverless-2016-10-31'
 4
 5Parameters:
 6  DomainName:
 7    Description: Root Domain
 8    Type: String
 9  SubDomain:
10    Description: Subdomain
11    Type: String
12  CfSubDomainName:
13    Description: Cloudfront domain name
14    Type: String
15  CfApexDomainName:
16    Description: Cloudfront domain name
17    Type: String
18
19Resources:
20  SubDomainRoute53RecordSetGroup:
21    Type: AWS::Route53::RecordSetGroup
22    Properties:
23      HostedZoneName: !Sub '${DomainName}.'
24      RecordSets:
25        - Name: !Sub '${SubDomain}.${DomainName}'
26          Type: 'A'
27          AliasTarget:
28            DNSName: !Ref CfSubDomainName
29            EvaluateTargetHealth: false
30            # The  following HosteZoneId is always used for alias records pointing to CF.
31            HostedZoneId: 'Z2FDTNDATAQYW2'
32
33  ApexDomainRoute53RecordSetGroup:
34    Type: AWS::Route53::RecordSetGroup
35    Properties:
36      HostedZoneName: !Sub '${DomainName}.'
37      RecordSets:
38        - Name: !Sub '${DomainName}'
39          Type: 'A'
40          AliasTarget:
41            DNSName: !Ref CfApexDomainName
42            EvaluateTargetHealth: false
43            # The  following HosteZoneId is always used for alias records pointing to CF.
44            HostedZoneId: 'Z2FDTNDATAQYW2'
45
46Outputs:
47  SubDomainRoute53RecordSetGroup:
48    Description: sub domain route 53
49    Value: !Ref SubDomainRoute53RecordSetGroup
50
51  ApexDomainRoute53RecordSetGroup:
52    Description: domain route 53
53    Value: !Ref ApexDomainRoute53RecordSetGroup
Info

How to execute

Step 1 Create a bucket to store cloudformation templates

1 aws s3 mb s3://cf-example-dev
2 

Step 2 Package template

1aws --region us-east-1 cloudformation package \
2    --template-file ./infrastructure/main.yaml \
3    --s3-bucket cf-example-dev \
4    --output-template-file packaged.template

Step 3 Deploy

1aws --region us-east-1 cloudformation deploy \
2    --stack-name cf-example-dev-stack \
3    --template-file packaged.template \
4    --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \
5    --parameter-overrides  DomainName=example.com SubDomain=www HostedZoneId=<ROUTE_53_HOSTED_ZONE_ID>

Deploying website content

Assuming public folder contains your artefacts, you can deploy the content using

1aws s3 sync ./public s3://www.example.com

Check complete code on github

You can check the complete code at GitHub.

comments powered by Disqus