基于aws和hexo的静态blog

基本的aws账号的set up

  • 申请AWS帐号,用邮箱和信用卡。这个申请的是root帐号。

  • 为root帐号申请软件mfa,我用的是iphone上装的google authenticator。以后登录的时候用两步验证。

  • (option)看一下iam user的最佳实践

  • 创建一个admin user,也enable mfa,给全部权限:

    1
    { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "iam:*", "Resource": "*" } }
  • root和admin只用aws console登录。

  • 创建一个developer user,给予power user access权限,并且enable access key,以后本地开发,比如用aws cli,就用这个aws access key。

  • 加一个policy,强制定期更新密码。

创建基础的pipeline

整个pipeline看起来如下图所示:

对应的cloudformation如下:

show code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
AWSTemplateFormatVersion: "2010-09-09"

Description: >
Used to create Pipeline.
CodeCommit => CodeBuild => S3 => Cloudformation

Parameters:
AppName:
Description: Name of the pipeline
Type: String
Default: HexoBlog
PipelineName:
Description: Name of the pipeline
Type: String
Default: HexoBlogPipeline
ArtifactS3BucketName:
Description: Name of the s3 buckt to store artifacts
Type: String
Default: blog-pipeline-artifacts
RepositoryBaseName:
Description: Base name of the git repository
Type: String
Default: HexoBlog
RepositoryBranchName:
Description: CodeCommit branch name
Type: String
Default: master
SNSSubscriptionEmail:
Description: The email address to recieve notification
Type: String
Default: "***@qq.com" # your email

Resources:
######################################################################################################################
# The S3 bucket to store artifacts
######################################################################################################################
ArtifactStoreBucket:
DeletionPolicy: Retain # keeps everything after s3 bucket is deleted
Type: AWS::S3::Bucket
Properties:
BucketName: {Ref: ArtifactS3BucketName}

CodePipelineSNSTopic:
Type: AWS::SNS::Topic
Properties:
Subscription:
- Endpoint: {Ref: SNSSubscriptionEmail}
Protocol: email-json

AssetsCodeRepository:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName:
Fn::Sub: "${RepositoryBaseName}Assets"
RepositoryDescription: >
This repository contains the hexo assets

CloudformationTemplatesCodeRepository:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName:
Fn::Sub: "${RepositoryBaseName}CloudformationTemplates"
RepositoryDescription: >
This repository contains the cloudformation templates to set up cloudfront.
AmazonCloudWatchEventRule:
DependsOn:
- AssetsCodeRepository
- CloudformationTemplatesCodeRepository
Type: 'AWS::Events::Rule'
Properties:
EventPattern:
source:
- aws.codecommit
detail-type:
- CodeCommit Repository State Change
resources:
- Fn::Join:
- ''
- - 'arn:aws:codecommit:'
- {Ref: 'AWS::Region'}
- ':'
- {Ref: 'AWS::AccountId'}
- ':'
- {"Fn::GetAtt" : ["AssetsCodeRepository", "Name"]}
- Fn::Join:
- ''
- - 'arn:aws:codecommit:'
- {Ref: 'AWS::Region'}
- ':'
- {Ref: 'AWS::AccountId'}
- ':'
- {"Fn::GetAtt" : ["CloudformationTemplatesCodeRepository", "Name"]}
detail:
event:
- referenceCreated
- referenceUpdated
referenceType:
- branch
referenceName:
- master
Targets:
- Arn:
Fn::Join:
- ''
- - 'arn:aws:codepipeline:'
- {Ref: 'AWS::Region'}
- ':'
- {Ref: 'AWS::AccountId'}
- ':'
- {Ref: Pipeline}
RoleArn: {"Fn::GetAtt" : ["AmazonCloudWatchEventRole", "Arn"]}
Id: codepipeline-Pipeline-autobuild
######################################################################################################################
# The CodeBuild projects to build two packages
######################################################################################################################
CloudformationTemplatesCodeBuildProject:
Properties:
Description: |
This code build project is used to build cloudformation templates
Artifacts:
Type: CODEPIPELINE
Environment:
ComputeType: BUILD_GENERAL1_SMALL
# EnvironmentVariables:
# - Name: BUILD_OUTPUT_BUCKET
# Value: BuildArtifactsBucket
Image: "aws/codebuild/standard:2.0"
Type: LINUX_CONTAINER
Name:
Fn::Sub: "${AppName}_cloudformation_templates_build"
ServiceRole: {"Fn::GetAtt" : ["CodeBuildServiceRole", "Arn"]}
Source:
Type: CODEPIPELINE
Type: "AWS::CodeBuild::Project"
AssetsCodeBuildProject:
Properties:
Description: |
This code build project is used to build hexo static assets
Artifacts:
Type: CODEPIPELINE
Environment:
ComputeType: BUILD_GENERAL1_SMALL
# EnvironmentVariables:
# - Name: BUILD_OUTPUT_BUCKET
# Value: BuildArtifactsBucket
Image: "aws/codebuild/standard:2.0"
Type: LINUX_CONTAINER
Name:
Fn::Sub: "${AppName}_assets_build"
ServiceRole: {"Fn::GetAtt": ["CodeBuildServiceRole", "Arn"] }
Source:
Type: CODEPIPELINE
Type: "AWS::CodeBuild::Project"

######################################################################################################################
# The real pipeline definition
######################################################################################################################
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
ArtifactStore:
Location: {Ref: ArtifactStoreBucket}
Type: S3
DisableInboundStageTransitions: []
Name: {Ref: PipelineName}
RoleArn: {"Fn::GetAtt" : ["CodePipelineExecutionRole", "Arn"]}
Stages:
- Name: CodeCommitSource
Actions:
- Name: CloudformationTemplatesCodeRepository
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: 1
Configuration:
BranchName: {Ref: RepositoryBranchName}
RepositoryName: {'Fn::GetAtt': [CloudformationTemplatesCodeRepository, Name]}
PollForSourceChanges: false # see https://docs.aws.amazon.com/codepipeline/latest/userguide/run-automatically-polling.html
# we use cloudwatch event to trigger pipeline other than let it poll
OutputArtifacts:
- Name: CloudformationTemplatesSourceArtifacts
- Name: AssetsCodeRepository
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: 1
Configuration:
BranchName: {Ref: RepositoryBranchName}
RepositoryName: {'Fn::GetAtt': [AssetsCodeRepository, Name]}
PollForSourceChanges: false # see https://docs.aws.amazon.com/codepipeline/latest/userguide/run-automatically-polling.html
# we use cloudwatch event to trigger pipeline other than let it poll
OutputArtifacts:
- Name: AssetsSourceArtifacts
- Name: Build
Actions:
- Name: BuildAssets
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
InputArtifacts:
- Name: AssetsSourceArtifacts
OutputArtifacts:
- Name: AssetsBuildArtifacts
Configuration:
ProjectName: {Ref: AssetsCodeBuildProject}
RunOrder: 1 # The assets package depend on nothing, so build it first.
- Name: BuildCloudformationTemplate
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
InputArtifacts:
- Name: CloudformationTemplatesSourceArtifacts
OutputArtifacts:
- Name: CloudformationTemplatesBuildArtifacts
Configuration:
ProjectName: {Ref: CloudformationTemplatesCodeBuildProject}
RunOrder: 1
- Name: Beta
Actions:
- Name: CreateBetaChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: 1
Configuration:
ActionMode: CHANGE_SET_REPLACE
Capabilities: CAPABILITY_IAM
ChangeSetName:
Fn::Sub: "${AppName}-ChangeSet-Beta"
ParameterOverrides: |
{
"SourceBucket": {
"Fn::GetArtifactAtt": ["CloudformationTemplatesBuildArtifacts", "BucketName"]
},
"SourceArtifactKey": {
"Fn::GetArtifactAtt": ["CloudformationTemplatesBuildArtifacts", "ObjectKey"]
},
"AssetsSourceBucket": {
"Fn::GetArtifactAtt": ["AssetsBuildArtifacts", "BucketName"]
},
"AssetsSourceArtifactKey": {
"Fn::GetArtifactAtt": ["AssetsBuildArtifacts", "ObjectKey"]
},
"Stage": "beta",
"ExportBetaForTest": "true"
}
RoleArn: {"Fn::GetAtt" : ["CloudFormationExecutionRole", "Arn"]}
StackName:
Fn::Sub: "${AppName}-Stack-Beta"
TemplatePath: "CloudformationTemplatesBuildArtifacts::cloudformation.yaml"
InputArtifacts:
- Name: CloudformationTemplatesBuildArtifacts
- Name: AssetsBuildArtifacts
RunOrder: 1
- Name: ExecuteBetaChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: 1
Configuration:
ActionMode: CHANGE_SET_EXECUTE
ChangeSetName:
Fn::Sub: "${AppName}-ChangeSet-Beta"
RoleArn: {"Fn::GetAtt" : ["CloudFormationExecutionRole", "Arn"]}
StackName:
Fn::Sub: "${AppName}-Stack-Beta"
OutputArtifacts:
- Name:
Fn::Sub: "${AppName}BetaChangeSet"
RunOrder: 2
- Name: Prod
Actions:
- Name: DeploymentApproval
ActionTypeId:
Category: Approval
Owner: AWS
Provider: Manual
Version: 1
Configuration:
NotificationArn: {Ref: CodePipelineSNSTopic}
CustomData: |
Do you want to update the prod stack?
RunOrder: 1
- Name: BlockBetaPortal
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CREATE_UPDATE
Capabilities: CAPABILITY_IAM
RoleArn: {"Fn::GetAtt" : ["CloudFormationExecutionRole", "Arn"]}
ParameterOverrides: |
{
"SourceBucket": {
"Fn::GetArtifactAtt": ["CloudformationTemplatesBuildArtifacts", "BucketName"]
},
"SourceArtifactKey": {
"Fn::GetArtifactAtt": ["CloudformationTemplatesBuildArtifacts", "ObjectKey"]
},
"AssetsSourceBucket": {
"Fn::GetArtifactAtt": ["AssetsBuildArtifacts", "BucketName"]
},
"AssetsSourceArtifactKey": {
"Fn::GetArtifactAtt": ["AssetsBuildArtifacts", "ObjectKey"]
},
"Stage": "beta",
"ExportBetaForTest": "false"
}
StackName:
Fn::Sub: "${AppName}-Stack-Beta"
TemplatePath: "CloudformationTemplatesBuildArtifacts::cloudformation.yaml"
InputArtifacts:
- Name: CloudformationTemplatesBuildArtifacts
- Name: AssetsBuildArtifacts
RunOrder: 2
- Name: DeployToProd
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: 1
Configuration:
ActionMode: CREATE_UPDATE
Capabilities: CAPABILITY_IAM
RoleArn: {"Fn::GetAtt": ["CloudFormationExecutionRole", "Arn"]}
ParameterOverrides: >
{
"SourceBucket": {
"Fn::GetArtifactAtt": ["CloudformationTemplatesBuildArtifacts", "BucketName"]
},
"SourceArtifactKey": {
"Fn::GetArtifactAtt": ["CloudformationTemplatesBuildArtifacts", "ObjectKey"]
},
"AssetsSourceBucket": {
"Fn::GetArtifactAtt": ["AssetsBuildArtifacts", "BucketName"]
},
"AssetsSourceArtifactKey": {
"Fn::GetArtifactAtt": ["AssetsBuildArtifacts", "ObjectKey"]
},
"Stage": "prod",
"ExportBetaForTest": "true"
}
StackName:
Fn::Sub: "${AppName}-Stack-Prod"
TemplatePath: "CloudformationTemplatesBuildArtifacts::cloudformation.yaml"
InputArtifacts:
- Name: CloudformationTemplatesBuildArtifacts
- Name: AssetsBuildArtifacts
OutputArtifacts:
- Name:
Fn::Sub: "${AppName}ProdChangeSet"
RunOrder: 3


######################################################################################################################
# IAM Policies & Roles
######################################################################################################################
CodeBuildServiceRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Version: "2012-10-17"
Path: /
Policies:
- PolicyDocument:
Statement:
- Sid: CloudWatchLogsPolicy
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Effect: Allow
Resource:
- Fn::Sub: "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*"
- Sid: CodeCommitPolicy
Action:
- "codecommit:GitPull"
Effect: Allow
Resource: "*"
- Sid: S3GetObjectPolicy
Action:
- "s3:GetObject"
- "s3:GetObjectVersion"
Effect: Allow
Resource:
- Fn::Sub: "arn:aws:s3:::${ArtifactStoreBucket}/*"
- Sid: S3PutObjectPolicy
Action:
- "s3:PutObject"
Effect: Allow
Resource:
- Fn::Sub: "arn:aws:s3:::${ArtifactStoreBucket}/*"
- Action:
- "cloudformation:DescribeStacks"
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: CodeBuildAccess
CodePipelineExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Version: "2012-10-17"
Path: /
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AWSCodeCommitFullAccess"
- "arn:aws:iam::aws:policy/AWSCodeDeployFullAccess"
- "arn:aws:iam::aws:policy/AmazonS3FullAccess"
- "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
- "arn:aws:iam::aws:policy/AmazonSNSFullAccess"
- "arn:aws:iam::aws:policy/AmazonSQSFullAccess"
- "arn:aws:iam::aws:policy/AmazonECS_FullAccess"
- "arn:aws:iam::aws:policy/AWSOpsWorksFullAccess"
- "arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess"
Policies:
-
PolicyDocument:
Statement:
-
Action:
- "iam:PassRole"
- "elasticbeanstalk:*"
- "cloudformation:*"
- "ecr:DescribeImages"
- "lambda:InvokeFunction"
- "lambda:ListFunctions"
- "lambda:InvokeAsyc"
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: CodePipelineInlinePolicy
CloudFormationExecutionRole:
Properties:
AssumeRolePolicyDocument:
Statement:
Action:
- "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- cloudformation.amazonaws.com
Version: "2012-10-17"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AdministratorAccess"
Path: /
Type: "AWS::IAM::Role"
AmazonCloudWatchEventRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action: 'sts:AssumeRole'
Path: /
Policies:
- PolicyName: cwe-pipeline-execution
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: 'codepipeline:StartPipelineExecution'
Resource:
Fn::Join:
- ''
- - 'arn:aws:codepipeline:'
- {Ref: 'AWS::Region'}
- ':'
- {Ref: 'AWS::AccountId'}
- ':'
- {Ref: 'Pipeline'}

pipeline解释

  • 有两个package,一个是用来生成static assets(html, js等)的,另一个是负责创建cloudfront相关的cloudformation templates。
  • 这两个package用code build构建之后,先部署到beta的cloudformation stack,然后人工approve后部署到prod的cloudformation stack。

域名、certificate

  • 我想用自己的域名,于是去route53申请了一个。申请之后会自动创建一个hosted zone,在里面可以创建record把子域名绑定到prod的cloudfront上。
  • 为了用https,需要去ACM里申请一个certificate

static assets package

  • 我用的hexo来创建静态的blog页面。对应的buildsspec文件如下:
show code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: 0.2
phases:
install:
runtime-versions:
nodejs: 10
commands:
- echo installing dependencies...
- npm install -g hexo-cli
- npm install
- echo finished installing dependencies!
# pre_build: use this phase to run tests
build:
commands:
- echo generating static assets...
- hexo generate
- echo finished generating static assets!
post_build:
commands:
- echo copying favicon.png...
- cp favicon.png public/favicon.png
- echo finished copying favicon.png!
artifacts:
files:
- '**/*'
base-directory: public

  • hexo默认的theme,用了一些google的字体、jquery cdn等。众所周知,需要替换。把themes/landscape/layout/_partial下面的文件,一个是after-footer.ejs,一个是head.ejs,里面的google url自己找国内对应的替换。
  • cloudfront不能把指向sub dir的url自动重定向到sub dir下面的index.html,所以要在生成静态文件的时候一步到位。加一个script(/home/xiaoma/WebstormProjects/HexoBlogAssets/themes/landscape/scripts/url_for.js)来替换默认的url_for:
show code

1
2
3
4
5
6
7
8
9
10
11
12
      const _path = require('path');
const origin_url_for = hexo.extend.helper.get('url_for').bind(hexo);

hexo.extend.helper.register('url_for', function(path, options) {
origin_url = origin_url_for(path, options);
console.log(origin_url);
if (_path.extname(origin_url)) {
return origin_url;
} else {
return _path.join(origin_url, 'index.html')
}
});

cloudformation package

  • 这个package包含了cloudformation template和aws lambda用到的python代码。buildspec如下:
show code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: 0.2
phases:
install:
runtime-versions:
python: 3.7
commands:
- echo installing dependencies...
- mkdir artifacts
- pip install -r requirements.txt -t artifacts
- echo finished installing dependencies!
# pre_build: use this phase to run tests
build:
commands:
- echo packaging lambda...
- cp HexoBlogCloudFormationTemplates/lambda_handler.py artifacts/lambda_handler.py
- echo finished packaging lambda!
post_build:
commands:
- echo copying cloudformation templates...
- cp HexoBlogCloudFormationTemplates/configuration/cloudformation/blog.yaml artifacts/cloudformation.yaml
- echo finished preparing artifacts!
artifacts:
files:
- '**/*'
base-directory: artifacts

  • cloudformation包括了将static assets从code build的output复制到cloudfront bucket的lambda和cloudfront的配置。其中,当stage是prod的时候,cloudfront要绑定域名。
show code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
AWSTemplateFormatVersion: "2010-09-09"

Description: >
Create Cloudfront distribution for static assets in s3


Parameters:
SourceBucket: # s3 bucket of this package after build
Type: String
SourceArtifactKey: # s3 key of this package after build
Type: String
AssetsSourceBucket: # s3 bucket of assets after build
Type: String
AssetsSourceArtifactKey: # s3 key of assets after build
Type: String
Stage:
Type: String
Default: beta
AllowedValues: ["beta", "prod"]
ExportBetaForTest:
Type: String
AllowedValues: ["true", "false"]
DomainNames:
Type: CommaDelimitedList
Default: 'blog.02xiaoma.com, www.02xiaoma.com, www.blog.02xiaoma.com, 02xiaoma.com'
CertificateArn:
Type: String
Default: '' #arn of your acm certificate

Conditions:
ProdStage:
Fn::Equals :
- {"Ref" : "Stage"}
- prod

Outputs:
CloudfrontDomainName:
Value: {'Fn::GetAtt': [CloudFrontDistribution, DomainName]}


Resources:
######################################################################################################################
# The S3 bucket to store artifacts
######################################################################################################################
CloudfrontBucket:
Type: AWS::S3::Bucket

CloudfrontBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: {Ref: CloudfrontBucket}
PolicyDocument:
Statement:
- Action: ['s3:GetObject']
Effect: Allow
Principal:
CanonicalUser:
Fn::GetAtt: [Cloudfrontoriginaccessidentity, S3CanonicalUserId]
Resource: {'Fn::Sub': 'arn:aws:s3:::${CloudfrontBucket}/*'}
- Action: ['s3:ListBucket']
Effect: Allow
Principal:
CanonicalUser:
Fn::GetAtt: [Cloudfrontoriginaccessidentity, S3CanonicalUserId]
Resource: {'Fn::Sub': 'arn:aws:s3:::${CloudfrontBucket}'}
- Action: ['s3:*']
Effect: Allow
Principal:
AWS: {'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root'}
Resource: {'Fn::Sub': 'arn:aws:s3:::${CloudfrontBucket}/*'}

######################################################################################################################
# Custom resources to copy artifacts and invalidate cloud front distribution
######################################################################################################################
AssetsCopyLambda:
DependsOn:
- AssetsCopyLambdaRole
Type: AWS::Lambda::Function
Properties:
Description: copy assets into cloudfront bucket
Runtime: python3.7
Handler: lambda_handler.assets_copy_handler
MemorySize: 256
Timeout: 300
Code:
S3Bucket: {Ref: SourceBucket}
S3Key: {Ref: SourceArtifactKey}
Role:
Fn::GetAtt: [AssetsCopyLambdaRole, Arn]
AssetsCopyLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action: ['sts:AssumeRole']
Effect: Allow
Principal:
Service: [lambda.amazonaws.com]
ManagedPolicyArns: ['arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']
Policies:
- PolicyName: AssetsCopyLambdaPolicy
PolicyDocument:
Statement:
- Action:
- s3:*
- kms:*
Effect: Allow
Resource: '*'
AssetsCopyLambdaCustomResource:
Type: AWS::CloudFormation::CustomResource
DependsOn:
- AssetsCopyLambda
- CloudfrontBucket
- CloudfrontBucketPolicy
Properties:
DstBucket: {Ref: CloudfrontBucket}
SrcBucket: {Ref: AssetsSourceBucket}
SrcObjectKey: {Ref: AssetsSourceArtifactKey} # force run when static assets changed
ServiceToken:
Fn::GetAtt: [AssetsCopyLambda, Arn]

CloudfrontInvalidateLambda:
DependsOn:
- CloudfrontInvalidateLambdaRole
Type: AWS::Lambda::Function
Properties:
Description: Invalidate a cloudformation distribution
Runtime: python3.7
Handler: lambda_handler.cloudfront_invalidate_handler
MemorySize: 256
Timeout: 300
Code:
S3Bucket: {Ref: SourceBucket}
S3Key: {Ref: SourceArtifactKey}
Role:
Fn::GetAtt: [CloudfrontInvalidateLambdaRole, Arn]
CloudfrontInvalidateLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action: ['sts:AssumeRole']
Effect: Allow
Principal:
Service: [lambda.amazonaws.com]
ManagedPolicyArns: ['arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']
Policies:
- PolicyName: CloudfrontInvalidateLambdaPolicy
PolicyDocument:
Statement:
- Action: ['cloudfront:CreateInvalidation']
Effect: Allow
Resource: '*'
CloudfrontInvalidateCustomResource:
Type: AWS::CloudFormation::CustomResource
DependsOn:
- CloudfrontInvalidateLambda
Properties:
OriginAssets: # force run when cloudfront origin path changed
Fn::GetAtt: [AssetsCopyLambdaCustomResource, OriginPath]
DistributionId: {Ref: CloudFrontDistribution}
ServiceToken:
Fn::GetAtt: [CloudfrontInvalidateLambda, Arn]


######################################################################################################################
# Cloudfront distribution
######################################################################################################################
CloudFrontDistribution:
DependsOn:
- CloudfrontBucket
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Aliases:
Fn::If:
- ProdStage
- {Ref: DomainNames}
- {Ref: 'AWS::NoValue'}
DefaultCacheBehavior:
AllowedMethods: [GET, HEAD, OPTIONS]
Compress: true
ForwardedValues:
Cookies: {Forward: none}
QueryString: 'false'
TargetOriginId: hexo_blog_static_assets
ViewerProtocolPolicy: redirect-to-https
DefaultRootObject: index.html
Enabled: true
HttpVersion: http2
Origins:
- DomainName:
Fn::GetAtt: [CloudfrontBucket, RegionalDomainName]
Id: hexo_blog_static_assets
OriginPath:
Fn::GetAtt: [AssetsCopyLambdaCustomResource, OriginPath]
S3OriginConfig:
OriginAccessIdentity:
Fn::Sub:
- origin-access-identity/cloudfront/${Id}
- Id: {Ref: Cloudfrontoriginaccessidentity}
PriceClass: PriceClass_200
ViewerCertificate:
AcmCertificateArn:
Fn::If:
- ProdStage
- {Ref: CertificateArn}
- {Ref: 'AWS::NoValue'}
CloudFrontDefaultCertificate:
Fn::If:
- ProdStage
- {Ref: 'AWS::NoValue'}
- true
SslSupportMethod:
Fn::If:
- ProdStage
- sni-only
- {Ref: 'AWS::NoValue'}

Cloudfrontoriginaccessidentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: string-value

  • 用到的lambda code:
show code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
from __future__ import print_function
import logging
import json
import uuid
import boto3
from botocore.client import Config
import requests
import time
import datetime
import zipfile
import mimetypes
from io import BytesIO

logger = logging.getLogger()
logger.setLevel(logging.INFO)
SUCCESS = 'SUCCESS'
FAILED = 'FAILED'


def custom_handler_decorator(func):
def __decorator(event, context):
logger.info(json.dumps(event))
response = {
'StackId': event['StackId'],
'RequestId': event['RequestId'],
'LogicalResourceId': event['LogicalResourceId'],
'Status': 'SUCCESS',
'Data': {}
}

if 'PhysicalResourceId' in event:
response['PhysicalResourceId'] = event['PhysicalResourceId']
else:
response['PhysicalResourceId'] = event['PhysicalResourceId'] = str(uuid.uuid4())

try:
response.update(func(event, context))
except Exception as e:
logger.error(e)
response['Status'] = 'FAILED'
response['Reason'] = 'Failed to run handler'

return send_response(event, response)
return __decorator


def send_response(event, response):
logger.info("sending response to cloudformation...")
logger.info(json.dumps(response))
if 'ResponseURL' in event and event['ResponseURL']:
body = json.dumps(response)
try:
headers = {'Content-Type': ''}
r = requests.put(event['ResponseURL'], data=body, headers=headers)
r.raise_for_status()
logger.info("finished sending response to clouformation")
except requests.exceptions.HTTPError as e:
logger.error("send response to CloudFormation failed with status %d" % e.response.status_code)
logger.error("response: %s" % e.response.text)
except requests.exceptions.RequestException as e:
logger.error("failed to reach CloudFormation: %s" % e)
else:
logger.error("missing ResponseURL in event")
raise ValueError("missing ResponseURL in event")


@custom_handler_decorator
def assets_copy_handler(event, context):
request_type = event['RequestType']
if request_type in ['Create', 'Update']:
mimetypes.init()
logger.info("copying static assets...")
s3_client = boto3.client('s3', config=Config(signature_version='s3v4'))
properties = event['ResourceProperties']
dst_bucket = properties['DstBucket']
src_bucket = properties['SrcBucket']
src_objectkey = properties['SrcObjectKey']
dst_objectpath = "{0}-{1}".format(datetime.datetime.utcnow().strftime('%Y-%m-%d-%H-%I-%M-%S'), str(uuid.uuid4()))
s3_client
logger.info("extracting static assets from s3://{0}/{1} to s3://{2}/{3}".format(src_bucket, src_objectkey, dst_bucket, dst_objectpath))
with zipfile.ZipFile(
BytesIO(
s3_client.get_object(Bucket=src_bucket, Key=src_objectkey)['Body'].read()
)
) as zipdir:
for filename in zipdir.namelist():
logger.info("copying {0} to s3://{1}/{2}/{0}".format(filename, dst_bucket, dst_objectpath))
mime_type = mimetypes.guess_type(filename)[0]
if mime_type is None:
if filename.endswith('.wasm'):
mime_type = 'application/wasm'
else:
mime_type = 'application/octet-stream'
if mime_type == 'text/html':
mime_type = 'text/html; charset=utf-8'
logger.info("guessed mime type of {0} is {1}".format(filename, mime_type))
extra_args = {
'ContentType': mime_type,
'CacheControl': 'max-age=31536000'
}
with zipdir.open(filename, 'r') as fp:
s3_client.upload_fileobj(fp, dst_bucket, "{0}/{1}".format(dst_objectpath, filename), extra_args)
return {'Data': {'OriginPath': "/{0}".format(dst_objectpath)}}
else:
return {}


@custom_handler_decorator
def cloudfront_invalidate_handler(event, context):
logger.info("invalidating cloudfront distribution...")
cloufront_client = boto3.client('cloudfront')
properties = event['ResourceProperties']
dist_id = properties['DistributionId']
object_paths = '/*'.split()
cloufront_client.create_invalidation(
DistributionId=dist_id,
InvalidationBatch={
'Paths': {
'Quantity': len(object_paths),
'Items': object_paths
},
'CallerReference': str(time.time())
}
)
return {}