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 thedefined
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
)
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.
To create the token, follow these steps:.
Enter a name for the token
Under Select scopes, choose Only select repositories to limit the token's access to specific repositories.
In the Repository permissions section, grant the appropriate permissions to the token, such as Metadata,Webhooks, etc.
Click Generate 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
- click on Store a new secret
- Select type
Other type of secret
- In the Key/Value pairs section, fill
github
as the key and paste deGithub Token Previously generated
as the value - click Next
- In the Configure secret fill enter a name for the secret
- 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..
}
}
-
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
})
}
}
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/**']
}
})
}
}
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)
}
}
}
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)
...
}
}
Allow AWS CodeBuild to invoke Lambda
emptyBucketLambda.grantInvoke({
grantPrincipal: new iam.ServicePrincipal('codebuild.amazonaws.com')
}, todoBuildProject.projectArn, props.env?.account)
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()
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
Below are the results after two pushes to the master branch:
__
🥳✨
We have reached the end of the article.
Thank you so much 🙂
Your can find the full source code on GitHub Repo↗
Top comments (1)
Good job!