Set up a CI/CD pipeline using AWS CodeCommit, CodeBuild, and CodeDeploy to test and deploy Node.js applications on EC2 instances. Follow this step-by-step guide to streamline your development workflow and deliver high-quality applications with reduced risk.

Step 1: Create an EC2 Role with S3 Access:
To allow EC2 instances to access pipeline build artifacts stored in S3, we need to create an EC2 role and attach appropriate permissions:

const ec2Role = new iam.Role(this, 'ec2Role', {
  assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
  inlinePolicies: {
    ec2policy: new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          actions: [
            "s3:GetObject",
            "s3:GetObjectVersion",
            "s3:ListBucket"
          ],
          resources: ['*']
        })
      ]
    })
  }
});

We can also narrow down the resource for limiting the access to buckets with artifacts only.

Step 2: Launch the EC2 Instance and Set Up CodeDeploy Agent and Node.js Runtime
Next, we’ll launch an EC2 instance and configure it with user data to install the CodeDeploy agent and Node.js runtime:

const instanceName = 'ec2Stack/smplInstance';

const ec2Instance = new ec2.Instance(this, 'ec2-instance', {
  instanceName: instanceName,
  // other instance configurations
  role: ec2Role,
});

ec2Instance.addUserData(`
            #!/bin/bash
            sudo apt update
            sudo apt install ruby-full -y
            sudo apt install wget -y
            sudo apt install npm -y
            cd /home/ubuntu
            wget https://aws-codedeploy-${this.region}.s3.${this.region}.amazonaws.com/latest/install
            chmod +x ./install
            sudo ./install auto
`);

This instanceName serves as a ‘Name’ tag for the EC2 instance. By associating the tag with the instance, it becomes easier to identify and manage the instance within your infrastructure.

When setting up the CodeDeploy deployment group, we can leverage this tag to include the EC2 instance in the deployment group. By specifying the instanceName in the ec2InstanceTags property of the deployment group, CodeDeploy can automatically include the instance in the group that matches the specified tag.

One crucial aspect of this process is that CodeDeploy uses an appspec.yml file located at the root of the Node.js application. This appspec.yml file acts as a blueprint, defining the sequence of deployment steps and instructions for CodeDeploy to follow.

Step 3: Importing an Existing CodeCommit Repository
We’ll import an existing CodeCommit repository to use as the source for our pipeline:

const repository = codecommit.Repository.fromRepositoryName(this, 'RepoImport', 'foo');

const sourceOutput = new Artifact('SourceArtifact');

const sourceStageAction = new CodeCommitSourceAction({
  actionName: 'SourceAction',
  repository: repository,
  branch: 'test/test',
  output: sourceOutput,
  trigger: CodeCommitTrigger.EVENTS,
});

When configuring the sourceStageAction, mention correct ‘branch’ that contains the latest and stable code for your application.

The ’trigger’ being ‘CodeCommitTrigger.EVENTS’ means that the pipeline will be triggered based on events that occur within the CodeCommit repository. These events could include code commits, branch creations, or other relevant activities.


Step 4: Creating a CodeBuild Project
Now, we’ll create a CodeBuild project to build and test the application:


const codeBuildProject = new codebuild.PipelineProject(this, 'CodeBuildSmplProject', {
  projectName: 'BuildAndTest',
  buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec.yml'),
  cache: codebuild.Cache.local(codebuild.LocalCacheMode.DOCKER_LAYER),
  environment: {
    buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
    computeType: codebuild.ComputeType.SMALL,
    privileged: true,
  },
});

const buildOutput = new Artifact('BuildArtifact');

const codeBuildAction = new CodeBuildAction({
  actionName: 'CodeBuildAction',
  project: codeBuildProject,
  input: sourceOutput,
  outputs: [buildOutput],
  runOrder: 1,
});

When setting privileged: true in the CodeBuild project’s configuration, it allows the build environment to have elevated privileges during the build process. This is particularly useful when you need to perform Docker-related operations, such as running docker-compose commands, within the build pipeline. With elevated privileges, the build environment gains access to the host’s Docker daemon, enabling it to execute Docker commands effectively.

To define the steps of building and testing the code, you’ll create a ‘buildspec.yml’ file at the root of your Node.js application codebase. This YAML file acts as the blueprint for the build process, outlining the series of commands and actions to be executed during the build. You can specify steps such as installing dependencies, running tests, and building the application in the buildspec.yml file.


Step 5: Creating a CodeDeploy Role
For deployment, we need to create a CodeDeploy role and attach the necessary policy:

const codeDeployRole = new iam.Role(this, 'CodeDeployRole', {
  assumedBy: new iam.ServicePrincipal('codedeploy.amazonaws.com'),
});

codeDeployRole.addManagedPolicy(
  iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess'),
);

In this step, codedeployrole allows CodeDeploy to access the build artifacts stored in Amazon S3.


Step 6: Creating a CodeDeploy Application and Deployment Group
Create a CodeDeploy application and specify the EC2 instance name in the deployment group


const deployApp = new codedeploy.ServerApplication(this, 'CodeDeployApplicaiton', {
  applicationName: 'SmplDeployApp',
});

const deployGroup = new codedeploy.ServerDeploymentGroup(this, 'DeploymentGroup', {
  application: deployApp,
  ec2InstanceTags: new codedeploy.InstanceTagSet({
    Name: [instanceName],
  }),
  deploymentConfig: codedeploy.ServerDeploymentConfig.ALL_AT_ONCE,
  role: codeDeployRole,
});

By specifying ec2Instancetags in deployGroup, CodeDeploy can automatically trigger the deployment on the EC2 instance(s) that match the specified tags. This ensures that the deployment is targeted to the correct EC2 instance(s) without the need for manual intervention.

One crucial aspect of this process is that CodeDeploy uses an appspec.yml file located at the root of the Node.js application. This appspec.yml file acts as a blueprint, defining the sequence of deployment steps and instructions for CodeDeploy to follow.


Step 7: Creating the Pipeline
Finally, create the CI/CD pipeline with the defined stages and actions:

new Pipeline(this, 'WholePipeline', {
  pipelineName: 'CodePipeline',
  crossAccountKeys: false,
  stages: [
    {
      stageName: 'Source', actions: [sourceStageAction]
    },
    {
      stageName: 'Test', actions: [codeBuildAction]
    },
    {
      stageName: 'Deploy', actions: [deployAction]
    }
  ]
});

By following these steps, you'll have a fully functional CI/CD pipeline, empowering your development team to deliver high-quality software faster and with reduced risk. Embrace the power of CI/CD pipelines for a more efficient and effective development workflow.