DEV Community

Cover image for Setting up Github Integration with AWS CodeBuild using Cloud Development Kit(CDK) : Automating Angular App Deployment to S3
Kevin Lactio Kemta
Kevin Lactio Kemta

Posted on

Setting up Github Integration with AWS CodeBuild using Cloud Development Kit(CDK) : Automating Angular App Deployment to S3

Day 012 - 100DaysAWSIaCDevopsChallenge

In this article, I am going to create a new stack to create a AWS CodeBuild for building and deploying an Angular Application from a GitHub repository. Since the the application will be deployed as a static website within an S3 Bucket, configure AWS CodeBuild tp clear the entire bucket before deploying the latest version of the app.

The process will involve the following steps:

  • Configure the GitHub Repository to trigger CodeBuid on each event in the defined branch
    Create a Personal Access in Github concerned project which will be use by AWS CodeBuild project to create and configure a webhook, whenever there is a new commit or pull request to the defined branch.

  • Set Up AWS Codebuild with Build and Deployment stages
    Create a CodeBuild project that compiles and builds the Angular applications. After the build process, we'll include a deployment stage where the built files are uploaded tp the designated S3 Bucket. During this setup, we will also configure an AWS Lambda function that will handle the cleanup of the S3 Bucket before deploying the new version of app.

  • Deploy the stack
    Finally, we will deploy the stack using CDK command line, will will provision the resources (CodeBuild, Lambda, CloudWatch, Role and Permission)

Infrastructure Diagram (updated)

Diagram

Configure the GitHub Repository

Create Personal access token

AWS CodeBuild requires a GitHub Personal Access Token to establish a connection between CodeBuild and the GitHub project.

Create Personal access token page

To create the token, follow these steps:.

  1. go to Github Personal Access Tokeb page↗

  2. Enter a name for the token

  3. Under Select scopes, choose Only select repositories to limit the token's access to specific repositories.

  4. In the Repository permissions section, grant the appropriate permissions to the token, such as Metadata,Webhooks, etc.

  5. Click Generate token

github personal access token

Secrets Manager

Now that the Personal Access Token is generated, it’s best practice and more secure to avoid hardcoding sensitive information such as passwords or secret keys directly in the code. AWS recommends storing secrets in Secrets Manager or Parameter Store. In this case, we will store the GitHub Personal Access Token in AWS Secrets Manager for secure and convenient retrieval when needed.

To store your GitHub Token in the Secrets Manager AWS using console, follow these steps

  1. click on Store a new secret
  2. Select type Other type of secret
  3. In the Key/Value pairs section, fill github as the key and paste de Github Token Previously generated as the value
  4. click Next
  5. In the Configure secret fill enter a name for the secret
  6. click Next then Next, and finally Store

Set Up AWS Codebuild with Build and Deployment stages

Load Github credential
interface DeveloperToolsProps extends StackProps {
  git: {
    repositoryName: string,
    owner: string,
    branchRef?: string,
    accessTokenSecretArn?: string
  },
  build: {
    todoAppBucketName: string
  }
}
export class DeveloperToolsStack extends BaseStack {
  constructor(scope: Construct, id: string, props: DeveloperToolsProps) {
    super(scope, id, props)
    const secret = secrets.Secret.fromSecretAttributes(this, 'Github-Personal-Access-Token', {
        secretCompleteArn: props.git.accessTokenSecretArn
    })
    new codebuild.GitHubSourceCredentials(this, 'LoadGitHubCredentials', {
      accessToken: secret.secretValueFromJson('github')
    })
    // setup stack..
  }
}
Enter fullscreen mode Exit fullscreen mode
  • secret - The Github personal access token
  • GitHubSourceCredentials - Credentials used to authenticate with GitHub within the current AWS account and region.
Create LogGroup for CodeBuild reports logs
export class DeveloperToolsStack extends BaseStack {
  constructor(scope: Construct, id: string, props: DeveloperToolsProps) {
    super(scope, id, props)
    ...
    const cloudwatch = new logs.LogGroup(this, 'CodeBuildLogGroup', {
      logGroupName: 'CodeBuildLogGroup',
      retention: logs.RetentionDays.ONE_DAY,
      logGroupClass: logs.LogGroupClass.STANDARD,
      removalPolicy: RemovalPolicy.DESTROY
    })
  }
}
Enter fullscreen mode Exit fullscreen mode
Set up AWS CodeBuid Project
export class DeveloperToolsStack extends BaseStack {
  constructor(scope: Construct, id: string, props: DeveloperToolsProps) {
    super(scope, id, props)
    ...
    const emptyBucketLambda = new LambdaFunction(...);

    const todoBuildProject = new codebuild.Project(this, 'CodeBuild', {
      projectName: 'TodoAppCodeBuildProject',
      buildSpec: this.buildSpec(),
      logging: {
        cloudWatch: {
          logGroup: cloudwatch,
          enabled: true
        }
      },
      source: codebuild.Source.gitHub({
        repo: props.git.repositoryName,
        reportBuildStatus: true,
        owner: props.git.owner,
        webhook: true,
        webhookFilters: [
            codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH, codebuild.EventAction.PULL_REQUEST_MERGED)
            .andBranchIs(props.git.branchRef ?? 'master')
            .andFilePathIs('apps')
        ]
      }),
      visibility: codebuild.ProjectVisibility.PUBLIC_READ,
      environment: {
        buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_5,
        computeType: codebuild.ComputeType.SMALL,
        privileged: true
      },
      environmentVariables: {
        BUCKET_NAME: {
          type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          value: props.build.todoAppBucketName
        },
        EMPTY_BUCKET_FUNCTION_ARN: {
          type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          value: emptyBucketLambda.functionArn
        }
      },
      badge: false,
      role: new iam.Role(this, 'BuildCodeRole', {
        path: '/',
        assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com', {
          region: props.env?.region
        }),
        inlinePolicies: {
          logging: new iam.PolicyDocument({
            assignSids: true,
            statements: [
              new iam.PolicyStatement({
                effect: iam.Effect.ALLOW,
                actions: [
                  'logs:CreateLogGroup',
                  'logs:PutLogEvents',
                  'logs:CreateLogStream'
                ],
                resources: [cloudwatch.logGroupArn]
              })
            ]
          }),
          s3: new iam.PolicyDocument({
            assignSids: true,
            statements: [
              new iam.PolicyStatement({
                effect: iam.Effect.ALLOW,
                actions: ['s3:PutObject'],
                resources: [
            `arn:aws:s3:::${props.build.todoAppBucketName}`,
            `arn:aws:s3:::${props.build.todoAppBucketName}/*`
                ]
              })
            ]
          }),
          secrets: new iam.PolicyDocument({
            assignSids: true,
            statements: [
              new iam.PolicyStatement({
                effect: iam.Effect.ALLOW,
                resources: [props.git.accessTokenSecretArn!],
                actions: ['secretsmanager:GetSecretValue']
              })
            ]
          }),
          lambda_function: new iam.PolicyDocument({
            assignSids: true,
            statements: [
              new iam.PolicyStatement({
                effect: iam.Effect.ALLOW,
                actions: [
                  'lambda:InvokeFunction'
                ],
                resources: [emptyBucketLambda.functionArn]
              })
            ]
          })
        }
      })
    })

  }

  buildSpec = (): codebuild.BuildSpec => {
    return codebuild.BuildSpec.fromObjectToYaml({
      version: '0.2',
      env: {
        shell: 'bash'
      },
      phases: {
        pre_build: {
          commands: [
            'echo Build started on `date`',
            'npm install -g @angular/cli',
            'cd apps/todo-app && npm install'
          ]
        },
        build: {
          commands: [
            'ng build --configuration production',
            `aws lambda invoke --function-name \${EMPTY_BUCKET_FUNCTION_ARN} --cli-binary-format raw-in-base64-out --payload '{ "payload": {"bucketName": "\${BUCKET_NAME}"} }' response.json`,
            'aws s3 cp dist/todo-app/browser s3://${BUCKET_NAME}/ --recursive'
          ]
        }
      },
      artifacts: {
        files: ['**/*']
      },
      cache: {
        paths: ['node_modules/**/*', 'dist/**']
      }
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

This snippet code sets up AWS CodeBuild to automatically build the application when specific events occur in a GitHub repository. It listens for pushes or merged pull requests on a specified master branch, but only triggers a build if changes are made in the apps directory. Additionally, it reports the build status back to GitHub to provide feedback directly within the GitHub interface.

  • source - Defines the source of the CodeBuild project : It specifies that the build source will be a Github repository
    • repo - Specifies the name of the GitHub repository that CodeBuild will use as the source
    • reportBuildStatus - Enables reporting the build status back to GitHub. GitHub will display the build status (e.g., success, failure)
    • webhook - Enable a webhook in the Github repository to automatically trigger the Codebuild project whenever some events occurs (PUSH and PULL REQUEST). This ensures continuous integration.
    • webhookFilters - Speficied the condidions under which the webhook will trigger the CodeBuild project.
  • Environment - Defines the environment in which CodeBuild will run (e.g: Operating system, computer resources, etc.)
    • buildImage - The build Image that CodeBuild will use, needs for Operating System.
    • computeType - The type of compute resources (CPC, Memory) allocated to the build environment.
    • privileged - Allows the build container to perform certain operations that require elevated permissions. It is require by Docker.
Setup Lambda to delete all objects in the bucket

As for new deployment, we need to clean up the bucket for a new version of code. This is the function code to clean a specified bucket.

// ./apps/functions/todo-app/empty-bucket.function.ts
const client = new S3Client({
  region: process.env.REGION ?? 'us-east-1'
})
const repo = new ObjectRepository(client)
export const handler: Handler = async (event: {
  payload: { bucketName: string }
}, context: Context, callback: Callback) => {
  if (!event.payload?.bucketName) {
    callback(new Error('Invalid payload'))
  } else {
    try {
      const response = await repo.deepGetAllObjects(event.payload.bucketName)
      await repo.deleteBucketObjects(event.payload.bucketName, response.objects.map(value => value.key))
      callback(null, response)
    } catch (e) {
      callback(e, null)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Find the full components of the function here↗

export class DeveloperToolsStack extends BaseStack {
  constructor(scope: Construct, id: string, props: DeveloperToolsProps) {
    super(scope, id, props)
    ...
    const emptyBucketLambda = new LambdaFunction(this, 'EmptyBucketFunction', {
      entryFunction: './apps/functions/todo-app/empty-bucket.function.ts',
      functionName: 'Empty-Bucket-Function',
      env: {
        REGION: props.env?.region!
      },
      role: {
        name: 'EmptyBucketFunctionRole',
        inlinePolicies: {
          s3: new iam.PolicyDocument({
            assignSids: true,
            statements: [
              new iam.PolicyStatement({
                effect: iam.Effect.ALLOW,
                actions: [
                  's3:GetBucket',
                  's3:GetObject',
                  's3:ListObjects',
                  's3:DeleteObject'
                ],
                resources: [
                  `arn:aws:s3:::${props.build.todoAppBucketName}`,
                  `arn:aws:s3:::${props.build.todoAppBucketName}/*`
                ]
              })
            ]
          })
        }
      },
      logged: true
    });
    emptyBucketLambda.resource.applyRemovalPolicy(RemovalPolicy.DESTROY)
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode
Allow AWS CodeBuild to invoke Lambda
emptyBucketLambda.grantInvoke({
    grantPrincipal: new iam.ServicePrincipal('codebuild.amazonaws.com')
}, todoBuildProject.projectArn, props.env?.account)
Enter fullscreen mode Exit fullscreen mode

Deploy the stack

const app = new cdk.App()
const devtoolsStack = new DeveloperToolsStack(app, 'Day012DevToolsStack', {
  env,
  git: {
    repositoryName: '100DaysTerraformAWSDevops',
    branchRef: 'master',
    owner: '<GITHUB_USERNAME>',
    accessTokenSecretArn: process.env.PERSONAL_ACCESS_SECRET_ARN
  },
  build: {
    todoAppBucketName: "<YourBucketName>"
  }
})
app.synth()
Enter fullscreen mode Exit fullscreen mode
git clone https://github.com/nivekalara237/100DaysTerraformAWSDevops.git

cd 100DaysTerraformAWSDevops/day_012
export PERSONAL_ACCESS_SECRET_ARN="arn:aws:secretsmanager:<REGION>:<ACCOUNT>:secret:<SECRET_ID>"

cdk deploy --profile cdk-user Day012DevToolsStack
Enter fullscreen mode Exit fullscreen mode

Below are the results after two pushes to the master branch:

CodeBuild Result

__

🥳✨
We have reached the end of the article.
Thank you so much 🙂


Your can find the full source code on GitHub Repo↗

Top comments (0)