React App CI/CD Pipeline with AWS CDK v2 in Python

React App CI/CD Pipeline with AWS CDK v2 in Python

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.

image.png

image.png

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.

  1. bucket_name argument is the physical name of the bucket. This name should be globally unique.
  2. public_read_access=True grant public read access to all objects in the bucket.
  3. website_index_document will enable static website hosting for this bucket and we will use it to provide the name of the index document.
  4. website_error_document will set the name of the error document.
  5. auto_delete_objects=True means that when the stack is deleted, all objects will be deleted automatically.
  6. 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.

image.png

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:

  1. Input: React Application code from GitHub.
  2. 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!

image.png

Thank you for reading!