Creating a CI/CD Pipeline for AWS Elastic Beanstalk with AWS CDK

Creating a CI/CD Pipeline for AWS Elastic Beanstalk with AWS CDK

ยท

7 min read

In this blog, I will provide a comprehensive guide on how to set up a Continuous Integration/Continuous Deployment (CI/CD) pipeline for an AWS Elastic Beanstalk application using the AWS Cloud Development Kit (CDK). I will break down the code into smaller sections, explaining each part's purpose.

Step 1: Project Structure and Setup

The project is structured into three primary files:

  1. stacks/eb-codepipeline-stack.ts

  2. config/config.yaml

  3. bin/main.ts

This organized structure makes it easier to manage the deployment process and its configurations.

๐Ÿ’ก
The project GitHub repo link can be found at the end of the blog.
  1. stacks/eb-codepipeline-stack.ts: This file contains the code to define the AWS infrastructure for your Elastic Beanstalk application and the CI/CD pipeline using AWS CDK.

  2. config/config.yaml: This file stores the configuration values for different environments, such as development (dev) and production (prod).

  3. bin/main.ts: This is the entry point for the CDK app, where we create the app and instantiate the CDK stack based on the environment specified.

Now, let's dive into the details of the code.

Step 2: Elastic Beanstalk Configuration

In the eb-codepipeline-stack.ts file, we start by configuring the Elastic Beanstalk environment. This is where your web application will be hosted. Let's break down the code:

Section 2.1: Asset Configuration

const webAppZipArchive = new Asset(this, 'expressjs-app-zip', {
  path: `${__dirname}/../express-app`,
});

Here, we create an asset named 'expressjs-app-zip' from your application code, which is stored in the 'express-app' directory.

Section 2.2: Application Creation

const app = new CfnApplication(this, 'eb-application', {
  applicationName: appName,
});

We define the Elastic Beanstalk application by providing it with a name.

Section 2.3: Application Version Configuration

const appVersionProps = new CfnApplicationVersion(this, 'eb-app-version', {
  applicationName: appName,
  sourceBundle: {
    s3Bucket: webAppZipArchive.s3BucketName,
    s3Key: webAppZipArchive.s3ObjectKey,
  },
});

appVersionProps.addDependency(app); // here we want to create app before its version

Here we're creating a version of your Elastic Beanstalk application, capturing the code from an S3 bucket and associating it with the application named 'appName'.

The appVersionProps.addDependency(app) line ensures that the Elastic Beanstalk Application is created before its associated Application Version.

Section 2.4: Create instance profile

 const instanceRole = new Role(
      this,
      `${appName}-aws-elasticbeanstalk-ec2-role`,
      {
        assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
      }
    );

    const managedPolicy = ManagedPolicy.fromAwsManagedPolicyName(
      'AWSElasticBeanstalkWebTier'
    );

    instanceRole.addManagedPolicy(managedPolicy);

    const instanceProfileName = `${appName}-instance-profile`;

    const instanceProfile = new CfnInstanceProfile(this, instanceProfileName, {
      instanceProfileName: instanceProfileName,
      roles: [instanceRole.roleName],
    });

Here we're creating an instance profile for the Elastic Beanstalk application with an AWS-managed policy 'AWSElasticBeanstalkWebTier' to ensure it has proper permissions.

Section 2.5: Create Option Setting properties

const optionSettingProperties: CfnEnvironment.OptionSettingProperty[] = [
      {
        namespace: 'aws:autoscaling:launchconfiguration',
        optionName: 'IamInstanceProfile',
        value: instanceProfileName,
      },
      {
        namespace: 'aws:autoscaling:asg',
        optionName: 'MinSize',
        value: minSize || DEFAULT_SIZE,
      },
      {
        namespace: 'aws:autoscaling:asg',
        optionName: 'MaxSize',
        value: maxSize || DEFAULT_SIZE,
      },
      {
        namespace: 'aws:ec2:instances',
        optionName: 'InstanceTypes',
        value: instanceTypes || DEFAULT_INSTANCE_TYPE,
      },
    ];

// here we're adding ssl properties if ARN defined
if (sslCertificateArn) {
      optionSettingProperties.push(
        {
          namespace: 'aws:elasticbeanstalk:environment',
          optionName: 'LoadBalancerType',
          value: 'application',
        },
        {
          namespace: 'aws:elbv2:listener:443',
          optionName: 'ListenerEnabled',
          value: 'true',
        },
        {
          namespace: 'aws:elbv2:listener:443',
          optionName: 'SSLCertificateArns',
          value: sslCertificateArn,
        },
        {
          namespace: 'aws:elbv2:listener:443',
          optionName: 'Protocol',
          value: 'HTTPS',
        }
      );
    }

Here we're setting an Option Setting Properties for the Elastic Beanstalk application environment.

Section 2.6: Create ElasticBeanstalk Environment

const ebEnvironment = new CfnEnvironment(this, 'eb-environment', {
      environmentName: envName,
      applicationName: appName,
      solutionStackName: '64bit Amazon Linux 2 v5.8.0 running Node.js 18',
      optionSettings: optionSettingProperties,
      versionLabel: appVersionProps.ref,
    });

Here we're creating an Elastic Beanstalk environment for hosting an application, specifying its name, associated application, runtime stack, and various configurations like instance types and SSL certificates.

Section 2.7: Output ElasticBeanstalk Environment (ALB) endpoint URL

new CfnOutput(this, 'eb-url-endpoint', {
      value: ebEnvironment.attrEndpointUrl,
      description: 'URL endpoint for the elasticbeanstalk',
    });

The above code sets up an application version, pointing to the asset created earlier, which represents your application code.

The code also includes creating an instance profile, defining environment options, handling SSL certificates if provided, and outputting URL.

Step 3: CodePipeline Configuration

Moving on to the CI/CD pipeline configuration. This code sets up a CodePipeline with Source, Build, and Deployment stages.

Section 3.1: Source Stage Configuration

const sourceOutput = new Artifact();
const sourceAction = new GitHubSourceAction({
  actionName: 'GitHub', // we're using github here, can be codecommit, bitbucket etc
  owner: githubRepoOwner,
  repo: githubRepoName,
  branch: branch,
  oauthToken: SecretValue.secretsManager(githubAccessTokenName),
  output: sourceOutput,
});

Here, we configure the source stage. It uses a GitHub repository as the source, which can be triggered based on the specified branch.

Section 3.2: Build Stage Configuration

const buildOutput = new Artifact();
const buildProject = new Project(this, 'codebuild-project', {
  buildSpec: BuildSpec.fromObject({
    version: '0.2',
    phases: {
      install: {
        commands: ['npm install'],
      },
      build: {
        commands: ['npm run build'],
      },
    },
    artifacts: {
      files: ['**/*'],
    },
  }),
  environment: {
    buildImage: LinuxBuildImage.STANDARD_5_0,
  },
});

In this section, we set up the Build stage. It defines a CodeBuild project with build specifications, including installing dependencies and building the application.

Section 3.3: Deployment Stage Configuration

const deployAction = new ElasticBeanstalkDeployAction({
  actionName: 'ElasticBeanstalk',
  applicationName: appName,
  environmentName: envName || 'eb-nodejs-app-environment',
  input: projectType === 'ts' ? buildOutput : sourceOutput,
});

The deployment stage is configured to deploy the application to Elastic Beanstalk. It depends on whether your project is TypeScript or JavaScript.

Step 4: Pipeline (CodePipeline) Creation

Finally, we create the pipeline by specifying the order of stages based on the project type:

const jsProject = [
  { stageName: 'Source', actions: [sourceAction] },
  { stageName: 'Deploy', actions: [deployAction] },
];

const tsProject = [
  { stageName: 'Source', actions: [sourceAction] },
  { stageName: 'Build', actions: [buildAction] },
  { stageName: 'Deploy', actions: [deployAction] },
];

const stages = projectType === 'ts' ? tsProject : jsProject;

const codePipeline = new Pipeline(this, 'codepipeline', {
  pipelineName: pipelineName,
  artifactBucket: getPipelineBucket,
  stages,
});

Here, we define the pipeline stages based on the project type. If it's TypeScript, it includes the Build stage; otherwise, it skips it.

Step 5: Configuration in config/config.yaml

The config.yaml file contains configuration values for your AWS CDK setup, specifically for different environments, such as dev (development) and prod (production). Here's a more detailed breakdown of the config.yaml file:

environmentType:   # define the type of environment (e.g., "dev" or "prod").
githubRepoName:    # provide the name of the GitHub repository for your source code (Nodjes).
githubRepoOwner:   # specify the owner of the GitHub repository.
githubAccessTokenName:  # name of the secret in AWS Secrets Manager storing your GitHub access token.
projectType: ts   # specify the project type as either "ts" (TypeScript) or "js" (JavaScript).

dev:   # configuration specific to the development environment.
  stackName:   # define the name of the CDK stack for the dev environment.
  branch:   # specify the branch to trigger the pipeline for the dev environment.
  pipelineBucket:   # name of the S3 bucket to store artifacts for the dev environment (make sure you create one before deploying)
  pipelineConfig:
    name:   # specify a name for the pipeline configuration in the dev environment.
  minSize: 1   # minimum size for the Elastic Beanstalk environment in the dev environment.
  maxSize: 1   # maximum size for the Elastic Beanstalk environment in the dev environment.
  instanceTypes: t2.micro   # Define the instance type for the Elastic Beanstalk environment in the dev environment.
  ebEnvName:   # specify the name of the Elastic Beanstalk environment in the dev environment.
  ebAppName:   # specify the name of the Elastic Beanstalk application in the dev environment.

# similarly, you can configure the "prod" environment with the same set of parameters.

In the config.yaml file, you can define configuration values for both development and production environments. These values are essential for customizing your AWS CDK application to suit different stages of your deployment pipeline. Make sure to adjust these settings according to your project requirements for each environment.

This configuration file plays a crucial role in the flexibility and customization of your CDK setup, allowing you to maintain separate configurations for different environments.

Step 6: App Synthesis and Deployment

In the main.ts file, we create a CDK app and instantiate the CDK stack based on the environment, either dev or prod:

if (devProps?.stackName?.length) {
  const devStackName = devProps.stackName;
  new EbCodePipelineStack(app, devStackName, devProps);
} else {
  const prodStackName = prodProps?.stackName;
  new EbCodePipelineStack(app, prodStackName, prodProps);
}

app.synth();

This code initializes the CDK app and the stack based on the chosen environment, allowing you to create a CI/CD pipeline for your Elastic Beanstalk application.

In conclusion, this blog post detailed the code used to create a CI/CD pipeline for AWS Elastic Beanstalk using AWS CDK. We walked through setting up Elastic Beanstalk, configuring the pipeline, and explained each section of the code to help you understand how to deploy your application with ease.

Here's the GitHub repo link to the code. ๐Ÿ‘‡

Did you find this article valuable?

Support Mikaeel Khalid by becoming a sponsor. Any amount is appreciated!

ย