Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .cloudformation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# CloudFormation

This directory contains the Infrastructure as a Code to deploy the project on AWS, for a minimal cost (near $0).

> Note:
> It only works without SSL and doesn't generate the friendly DNS records. For that purpose, you need to set a CloudFront Distribution that points to the S3 bucket website link as a backend, and redirect all HTTP to HTTPS. You'll also need to generate a Public Hosted Zone and a A (and AAAA) Alias record pointing to the CloudFront distribution. To make SSL work correctly you'll also need to generate a certificate with ACM (Amazon Certificate Manager) and put it in the CloudFront distribution.

## Prequisite

To deploy this infrastructure, you'll prealably need to:
* Set a GitHub Token with Read Only access to the repo
* Have a AWS account with an IAM user (with your Access Key and Secret Key) having CloudFormation rights (at least)
* Set a `incoming-webhook` in your Slack Team (and obviously have a Slack Team)

## Deploying to AWS

To deploy the CloudFormation Stack, I like to use a powerful tool which is [aws-shell](https://github.com/awslabs/aws-shell) which give an enhanced CLI for AWS.</br >
The command to deploy everything is (pretty long):

```
aws cloudformation deploy --template-file ci_infra.yml \
--stack-name <your-stack-name> \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
GitHubRepo=vue-blog-demo \
BuildBucket=<bucket-for-builds> \
BucketName=<bucket-for-website-hosting> \
CacheControlMaxAge=300 \
GitHubUser=<github-user> \
GitHubBranch=<your-branch> \
GitHubToken=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
SlackHook=xxxxxxx/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxx
```

This will create a CloudFormation Stack with the following resources:
* **AWS::CodeBuild::Project**: a CodeBuild project to build the project (instructions in the `buildspec.yml` in root directory of this repo)
* **AWS::IAM::Role**: a CodeBuild IAM role to make CodeDeploy able to execute itself (spawn a container and access to S3 build Bucket and S3 Website bucket)
* **AWS::CodePipeline::Pipeline**: a CodePipeline pipeline to listen to any change in the GitHub repository and trigger the build
* **AWS::IAM::Role**: a CodePipeline IAM role to make it able to call CodeBuild and push an artifact to the build bucket
* **AWS::S3::Bucket**: 2 buckets, one for the website, an other to store the artifacts
* **AWS::S3::BucketPolicy**: a bucket policy attached to the website bucket, to make it accessible to the whole world

## Update the Stack

To update the stack (if you make any change to it), just run the same command than for deploying without the parameters (if you don"t want to override any parameters):

```
aws cloudformation deploy --template-file ci_infra.yml \
--stack-name <your-stack-name> \
--capabilities CAPABILITY_NAMED_IAM
```
## Delete the Stack

Since everything is created with CloudFormation, you can delete every resources with one command, without forgeting any resources, avoiding aditional costs. The command is pretty simple:

```
aws cloudformation delete-stack --stack-name ci_infra.yml
```

That's all.
300 changes: 300 additions & 0 deletions .cloudformation/ci_cd_infra.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
AWSTemplateFormatVersion: "2010-09-09"
Description: CI/CD + S3 bucket Website Hostoing for Front-End example in VueJS

#-------------------------------------------------------------------------------
#--[ PARAMETERS ]---------------------------------------------------------------
#-------------------------------------------------------------------------------
Parameters:
BuildBucket:
Description: S3 Build bucket
Type: String

BucketName:
Description: S3 website bucket
Type: String

CacheControlMaxAge:
Description: Cache Control for S3 synchronization (default 5 min)
Type: Number
Default: 300

CertificateArn:
Description: Certificate ARN, located in us-east-1, for CloudFront Distribution CNAME
Type: String

GitHubUser:
Description: GitHub user
Type: String

GitHubRepo:
Description: GitHub repository
Type: String

GitHubBranch:
Description: Gith branch for
Type: String

GitHubToken:
Description: GitHub personal token
Type: String
NoEcho: true

HostedZoneName:
Description: Hosted Zone Name, obviously
Type: String

RecordName:
Description: Name of the record set (without the hosted zone name, just the last subdomain)
Type: String

SlackHook:
Description: Slack hook
Type: String
NoEcho: true

#-------------------------------------------------------------------------------
#--[ RESOURCES ]----------------------------------------------------------------
#-------------------------------------------------------------------------------
Resources:
S3WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref BucketName
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
# DeletionPolicy: Retain

S3BuildBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref BuildBucket

SampleBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3WebsiteBucket
PolicyDocument:
Statement:
- Action:
- s3:GetObject
Effect: Allow
Resource:
Fn::Join:
- ""
-
- "arn:aws:s3:::"
-
Ref: "S3WebsiteBucket"
- "/*"
Principal: "*"

CloudfrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Aliases:
- !Ref BucketName
Comment: !Ref BucketName
DefaultRootObject: index.html
Enabled: True
IPV6Enabled: True
DefaultCacheBehavior:
ForwardedValues:
QueryString: True
TargetOriginId: !Sub S3-Website-${BucketName}
ViewerProtocolPolicy: redirect-to-https
Origins:
- CustomOriginConfig:
OriginProtocolPolicy: http-only
DomainName: !Sub ${BucketName}.s3-website-${AWS::Region}.amazonaws.com
Id: !Sub S3-Website-${BucketName}
ViewerCertificate:
AcmCertificateArn: !Ref CertificateArn
SslSupportMethod: sni-only
Tags:
- Key: Env
Value: prod
- Key: Name
Value: static-website-example

Route53RecordSetIPv4:
Type: AWS::Route53::RecordSet
Properties:
AliasTarget:
DNSName: !GetAtt CloudfrontDistribution.DomainName
HostedZoneId: Z2FDTNDATAQYW2
Comment: !Ref BucketName
HostedZoneName: !Ref HostedZoneName
Name: !Sub ${RecordName}.${HostedZoneName}
Type: A

Route53RecordSetIPv6:
Type: AWS::Route53::RecordSet
Properties:
AliasTarget:
DNSName: !GetAtt CloudfrontDistribution.DomainName
HostedZoneId: Z2FDTNDATAQYW2
Comment: !Ref BucketName
HostedZoneName: !Ref HostedZoneName
Name: !Sub ${RecordName}.${HostedZoneName}
Type: AAAA

CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: AWSCodeBuildServiceRole
Path: /
AssumeRolePolicyDocument: |
{
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": [ "codebuild.amazonaws.com" ]},
"Action": [ "sts:AssumeRole" ]
}]
}
Policies:
- PolicyName: root
PolicyDocument:
Version: 2012-10-17
Statement:
- Resource: "*"
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- iam:*
- Resource:
- !Sub arn:aws:s3:::${BucketName}/*
- !Sub arn:aws:s3:::${BucketName}
Effect: Allow
Action:
- s3:*
- Resource:
- !Sub arn:aws:s3:::${BuildBucket}/*
- !Sub arn:aws:s3:::${BuildBucket}
Effect: Allow
Action:
- s3:Get*

CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: AWSCodePipelineServiceRole
Path: /
AssumeRolePolicyDocument: |
{
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": [ "codepipeline.amazonaws.com" ]},
"Action": [ "sts:AssumeRole" ]
}]
}
Policies:
- PolicyName: root
PolicyDocument:
Version: 2012-10-17
Statement:
- Resource:
- !Sub arn:aws:s3:::${BuildBucket}/*
- !Sub arn:aws:s3:::${BuildBucket}
Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketVersioning
- Resource: "*"
Effect: Allow
Action:
- codebuild:StartBuild
- codebuild:BatchGetBuilds
- codedeploy:*
- cloudformation:*
- iam:PassRole
- lambda:*

#--[ CodeBuild Production ]--------------------------------------------------------
CodeBuild:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: "CODEPIPELINE"
Source:
Type: "CODEPIPELINE"
Environment:
ComputeType: "BUILD_GENERAL1_SMALL"
Image: "node:8-alpine"
Type: "LINUX_CONTAINER"
EnvironmentVariables:
- Name: AWS_DEFAULT_REGION
Value: !Ref AWS::Region
- Name: BUCKET_NAME
Value: !Ref BucketName
- Name: CACHE_CONTROL_MAX_AGE
Value: !Ref CacheControlMaxAge
- Name: BRANCH
Value: !Ref GitHubBranch
- Name: SLACK_HOOK
Value: !Ref SlackHook
- Name: REPO
Value: !Ref GitHubRepo
- Name: USER
Value: !Ref GitHubUser
Name: !Ref GitHubRepo
ServiceRole: !GetAtt CodeBuildServiceRole.Arn

#--[ CodePipeline ]-------------------------------------------------------------
CodePipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Ref GitHubRepo
RoleArn: !GetAtt CodePipelineServiceRole.Arn
ArtifactStore:
Type: S3
Location: !Ref BuildBucket
Stages:
- Name: "Source"
Actions:
- Name: "Download_Source"
ActionTypeId:
Category: Source
Owner: ThirdParty
Version: 1
Provider: GitHub
Configuration:
Owner: !Ref GitHubUser
Repo: !Ref GitHubRepo
Branch: !Ref GitHubBranch
OAuthToken: !Ref GitHubToken
OutputArtifacts:
- Name: source
RunOrder: 1
- Name: "Build"
Actions:
- Name: "Build_and_deploy"
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName: !Ref CodeBuild
InputArtifacts:
- Name: source
OutputArtifacts:
- Name: build
RunOrder: 1

Outputs:
WebsiteURL:
Value: !GetAtt [S3WebsiteBucket, WebsiteURL]
Description: URL for website hosted on S3
Export:
Name: WebsiteURL
S3BucketSecureURL:
Value: !Join ['', ['https://', !GetAtt [S3WebsiteBucket, DomainName]]]
Description: Name of S3 bucket to hold website content
Export:
Name: WebsiteSecureURL
18 changes: 18 additions & 0 deletions buildspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: 0.1
os: linux
phases:
install:
commands:
- apk add --no-cache --update python3
- pip3 install awscli
- apk add curl
build:
commands:
- npm install
- npm run build
- aws s3 sync ./dist/ s3://$BUCKET_NAME/ --delete --cache-control max-age=$CACHE_CONTROL_MAX_AGE
post_build:
commands:
- 'curl -X POST -d "{\"text\": \"Environment deployment succeded\", \"attachments\": [{\"color\": \"#00CC00\",\"title\": \"Deployment informations\", \"text\": \"Repository: https://github.com/$USER/$REPO\nBranch: $BRANCH \nBucket: $BUCKET_NAME\"}]}" https://hooks.slack.com/services/$SLACK_HOOK'
- ls -lah ./
- echo "Sucess!"
2 changes: 1 addition & 1 deletion static/api/blog.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"results": [{
"title": "In Plain Vue",
"title": "A Cloud Enthousiast",
"post_label": "Exit reading mode",
"author_label": "View all authors"
}]
Expand Down
Loading