Create React App
To create a new React App with TypeScript, you can run:
yarn create react-app website --template typescript
Now let's create a GitHub repository for our React App.
Github
Repository
Go to GitHub, create a private repository and then push your react app code to it.
GitHub Personal Access Token
Create a personal access token that you will use to connect AWS CodePipeline to your GitHub account.
If you don't know how to create GitHub personal access tokens, you can follow this tutorial from GitHub docs.
You should give admin:repo_hook and repo scopes for this token.
Save this token because you will use it in a bit.
AWS CDK Application
First, install AWS CDK v2.13
npm install -g aws-cdk@2.13.0
Create Python CDK Project
You can create a new AWS CDK project by invoking cdk init
in an empty directory.
mkdir websitePipeline
cd websitePipeline
cdk init app --language python
Activate your virtualenv and install the required dependencies
source .venv/bin/activate
pip install -r requirements.txt
Now open this project in your favorite editor and let's code!
Breaking Down The Problem
If you want to deploy the website manually you need to download the source code, build the application and then copy everything in the build directory into the S3 Bucket and that's it!
Now we need to implement these steps using AWS services and we will create them using AWS CDK v2.
Let's start by creating an S3 Bucket that will host the React App.
Infrastructure Stack
Create S3 Bucket
Create infrastructure_stack.py
Python file. In this file create a class that inherit from aws_cdk.Stack
class.
from aws_cdk import (
Stack,
aws_s3,
RemovalPolicy,
CfnOutput
)
from aws_cdk.aws_s3 import IBucket
from constructs import Construct
class InfrastructureStack(Stack):
def __init__(self,
scope: Construct,
construct_id: str,
bucket_name: str,
**kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
In the constructor of this class, we will create all infrastructure resources. In our case, we will only create the S3 Bucket.
This function will create an S3 Bucket that will will be used to host a static website.
def __createS3WebsiteBucket(self, bucket_name: str) -> aws_s3.Bucket:
bucket = aws_s3.Bucket(self,
id="websiteBucket",
bucket_name=bucket_name,
public_read_access=True,
website_index_document="index.html",
website_error_document="index.html",
auto_delete_objects=True,
removal_policy=RemovalPolicy.DESTROY)
return bucket
Let's go line by line through this function.
bucket_name
argument is the physical name of the bucket. This name should be globally unique.public_read_access=True
grant public read access to all objects in the bucket.website_index_document
will enable static website hosting for this bucket and we will use it to provide the name of the index document.website_error_document
will set the name of the error document.auto_delete_objects=True
means that when the stack is deleted, all objects will be deleted automatically.removal_policy=RemovalPolicy.DESTROY
is the policy to apply when the bucket is removed from this stack.
Call this function in the constructor of the InfrastructureStack
, output the website URL and that's it.
from aws_cdk import (
Stack,
aws_s3,
RemovalPolicy,
CfnOutput
)
from aws_cdk.aws_s3 import IBucket
from constructs import Construct
class InfrastructureStack(Stack):
def __init__(self, scope: Construct, construct_id: str, bucket_name: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
self.bucket: IBucket = self.__createS3WebsiteBucket(bucket_name=bucket_name)
CfnOutput(self,
id="websiteURL",
value=self.bucket.bucket_website_url)
def __createS3WebsiteBucket(self, bucket_name: str) -> aws_s3.Bucket:
bucket = aws_s3.Bucket(self,
id="websiteBucket",
bucket_name=bucket_name,
public_read_access=True,
website_index_document="index.html",
website_error_document="index.html",
auto_delete_objects=True,
removal_policy=RemovalPolicy.DESTROY)
return bucket
Now go to app.py
file and instantiate the Infrastructure Stack.
import aws_cdk as cdk
from cdkexample.infrastructure_stack import InfrastructureStack
app = cdk.App()
infrastructure_stack = InfrastructureStack(app,
"InfrastructureStack",
bucket_name="www.alainelkhouryexample.com")
app.synth()
There's one step before deploying, we need to bootstrap our AWS environment first.
Bootstrapping AWS Environment
AWS CDK documentation describe bootstrapping AWS environment in the following way
Deploying AWS CDK apps into an AWS environment (a combination of an AWS account and region) may require that you provision resources the AWS CDK needs to perform the deployment. These resources include an Amazon S3 bucket for storing files and IAM roles that grant permissions needed to perform deployments. The process of provisioning these initial resources is called bootstrapping.
For more info, see AWS CDK docs
Run this command to bootstrap your AWS environment
cdk bootstrap
Deploying Infrastructure Stack
Now we're ready to deploy the app. Open a Terminal and run this command
cdk deploy --all --require-approval never
At this point you can go to your AWS console and check if the Infrastructure Stack is created successfully and try to access your website URL.
Now we have the infrastructure needed to host the website, let's focus on the CI/CD pipeline.
CI/CD Pipeline Stack
Create a new Python file, call it for example pipeline_stack.py
. In this stack we will create all CI/CD resources.
from aws_cdk import (
Stack,
aws_s3,
SecretValue
)
from aws_cdk.aws_codebuild import (
BuildEnvironment,
LinuxBuildImage,
ComputeType,
PipelineProject
)
from aws_cdk.aws_codepipeline import (
Artifact,
Pipeline
)
from aws_cdk.aws_codepipeline_actions import (
GitHubSourceAction,
CodeBuildAction,
S3DeployAction
)
from constructs import Construct
class PipelineStack(Stack):
def __init__(self, scope: Construct, construct_id: str, bucket: aws_s3.IBucket, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
GitHub Source Action
First, we need to grab the code from our GitHub repo into AWS CodePipeline. To do this, we need to create a GitHubSourceAction
.
NB: You should NEVER use oauth token in plain text, instead you should use AWS Secrets Manager. I used plain text here for simplicity.
source_artifact = Artifact(artifact_name="source_artifact")
github_source_action = GitHubSourceAction(action_name="Github_source_action",
oauth_token=SecretValue.plain_text("your-github-token"),
owner="your-github-username",
repo="github-repo-name",
branch="branch-name",
output=source_artifact)
Code Build
The next step is to build the react app. To do this we will use Code Build.
First we need to create a buildspec.yml
file inside our React App project. In this file we will define steps needed to build the application.
version: 0.2
phases:
install:
runtime-versions:
nodejs: 14
commands:
- npm install yarn
pre_build:
commands:
- yarn install
build:
commands:
- yarn build
artifacts:
files:
- "**/*"
discard-paths: no
base-directory: build
Create this file at the root directory of the React App and name it exactly buildspec.yml
.
Now we need to create a Code Build Project
Code Build Project
Code Build project need a build environment.
build_environment = BuildEnvironment(build_image=LinuxBuildImage.STANDARD_5_0,
compute_type=ComputeType.SMALL)
code_pipeline_project = PipelineProject(scope=self,
id="code_pipeline_project",
environment=build_environment,
project_name="code_build_project")
Code Build Action
Now using this Code Build project, we can create a Code Build Action that will be used by CodePipeline. This Action will take two Artifacts:
- Input: React Application code from GitHub.
- Output: Content in Build directory after building the React Application.
code_build_artifact = Artifact(artifact_name="code_build_artifact")
code_build_action = CodeBuildAction(action_name="build_action",
project=code_pipeline_project,
input=source_artifact,
outputs=[code_build_artifact])
S3 Deploy Action
Now we need an action that will take the output from Code Build Action and deploy it into our S3 Bucket. To do this, we will use S3 Deploy Action.
s3_deploy_action = S3DeployAction(action_name="s3_deploy_action",
bucket=bucket,
input=code_build_artifact)
AWS CodePipeline
At this point we have everything needed, we just need to wire them together and add them to the pipeline.
pipeline = Pipeline(self,
id="pipeline",
pipeline_name="websitePipeline")
pipeline.add_stage(stage_name="source",
actions=[github_source_action])
pipeline.add_stage(stage_name="build",
actions=[code_build_action])
pipeline.add_stage(stage_name="deploy",
actions=[s3_deploy_action])
Pipeline Stack
pipeline_stack.py
Python file should look like this
from aws_cdk import (
Stack,
aws_s3,
SecretValue
)
from aws_cdk.aws_codebuild import (
BuildEnvironment,
LinuxBuildImage,
ComputeType,
PipelineProject
)
from aws_cdk.aws_codepipeline import (
Artifact,
Pipeline
)
from aws_cdk.aws_codepipeline_actions import (
GitHubSourceAction,
CodeBuildAction,
S3DeployAction
)
from constructs import Construct
class PipelineStack(Stack):
def __init__(self, scope: Construct, construct_id: str, bucket: aws_s3.IBucket, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
pipeline: Pipeline = Pipeline(self,
id="pipeline",
pipeline_name="websitePipeline")
source_artifact = Artifact(artifact_name="source_artifact")
github_source_action = GitHubSourceAction(action_name="Github_source_action",
oauth_token=SecretValue.plain_text(
"your-github-token"),
owner="your-github-username",
repo="github-repo-name",
branch="branch-name",
output=source_artifact)
build_environment = BuildEnvironment(build_image=LinuxBuildImage.STANDARD_5_0,
compute_type=ComputeType.SMALL)
code_pipeline_project = PipelineProject(scope=self,
id="code_pipeline_project",
environment=build_environment,
project_name="code_build_project")
code_build_artifact = Artifact(artifact_name="code_build_artifact")
code_build_action = CodeBuildAction(action_name="build_action",
project=code_pipeline_project,
input=source_artifact,
outputs=[code_build_artifact])
s3_deploy_action = S3DeployAction(action_name="s3_deploy_action",
bucket=bucket,
input=code_build_artifact)
pipeline.add_stage(stage_name="source",
actions=[github_source_action])
pipeline.add_stage(stage_name="build",
actions=[code_build_action])
pipeline.add_stage(stage_name="deploy",
actions=[s3_deploy_action])
Now go again to app.py
and instantiate the pipeline stack.
import aws_cdk as cdk
from cdkexample.infrastructure_stack import InfrastructureStack
from cdkexample.pipeline_stack import PipelineStack
app = cdk.App()
infrastructure_stack = InfrastructureStack(app,
"InfrastructureStack",
bucket_name="www.alainelkhouryexample.com")
pipeline_stack = PipelineStack(app,
"PipelineStack",
bucket=infrastructure_stack.bucket)
app.synth()
Deploy Everything
cdk deploy --all --require-approval never
Congrats now your pipeline is ready!
Thank you for reading!