This is the second in a series of posts on automating blue-green deployments to Elastic Beanstalk using CodePipeline and Lambda.
In my previous post I outlined the design for a dynamic environment-creation process that can be used to support a blue-green deployment strategy in the context of automated deployments to Elastic Beanstalk (EB). In this post I’ll discuss how I went about implementing this process, using CodePipeline to orchestrate deployments, Lambda to execute environment-creation logic and the AWS SDK for JavaScript for interfacing with EB.
With this preamble out of the way, let’s dive into the first step in the process: determining the live environment.
(My code extracts will refer to some common code—this will be included at the end of the post.)
Determining the live environment
async function getLiveEnvironmentInfo() {
const environments = await describeEnvironments(ENVIRONMENT_NAMES);
if (environments.length === 1) {
const environment = environments[0];
return {
environmentName: environment.EnvironmentName,
domainName: environment.CNAME.split('.')[0]
};
} else {
warn('Unexpected number of environments found:', environments.length);
return null;
}
}
const liveEnvironmentInfo = await getLiveEnvironmentInfo();
// determine staging environment based on live environment
To determine the live environment we use the EB client to send a DescribeEnvironments command to EB. The command requires the name of the application and a list of valid environment names. Given there should only ever be one live environment, if the command returns a single environment we return that environment’s name and domain name; if the command returns less than one or more than one environment we return null. The caller can then use this information in the next step in the process: determining the staging environment.
Determining the staging environment
function getStagingEnvironmentInfo(liveEnvironmentInfo) {
let environmentName;
if (!liveEnvironmentInfo || liveEnvironmentInfo.environmentName === ENVIRONMENT_NAMES[1]) {
environmentName = ENVIRONMENT_NAMES[0];
} else if (liveEnvironmentInfo.environmentName === ENVIRONMENT_NAMES[0]) {
environmentName = ENVIRONMENT_NAMES[1];
} else {
warn('Invalid live environment name:', liveEnvironmentInfo.environmentName);
return null;
}
return {
environmentName: environmentName,
domainName: DOMAIN_NAME_STAGING
};
}
const stagingEnvironmentInfo = getStagingEnvironmentInfo(liveEnvironmentInfo);
if (!stagingEnvironmentInfo) {
throw new Error('Could not get staging environment info');
}
Given that there are only two valid environments, determining the staging environment involves simply interrogating the live environment and returning the other environment. If there is not a live environment or there is a live environment and the live environment’s name is ENVIRONMENT_NAMES[1], we return ENVIRONMENT_NAMES[0]. If there is a live environment and the live environment’s name is ENVIRONMENT_NAMES[0], we return ENVIRONMENT_NAMES[1]. If there is a live environment and the live environment’s name is not valid, we return null.
In the case where the staging environment cannot be determined, the caller throws an error; in cases where the staging environment can be determined, the caller receives an object containing the environment’s name and domain name.
Prior to continuing its execution the caller bundles the information about the environments into a single configuration object. The configuration object has the following shape:
{
['current' | 'next']: {
environmentName: <environmentName>,
domainName: <domainName>
}
}
The caller can then continue to the next step in the process: creating the application version.
Creating the application version
async function createApplicationVersion({ s3Bucket, s3Key, versionLabel }) {
const cmd = new CreateApplicationVersionCommand({
ApplicationName: APPLICATION_NAME,
Description: `Created for blue/green deployment at ${new Date().toISOString()}`,
SourceBundle: {
S3Bucket: s3Bucket,
S3Key: s3Key
},
VersionLabel: versionLabel
});
try {
await elasticBeanstalk.send(cmd);
return true;
} catch(err) {
error('Error creating application version', err);
return false;
}
}
const codePipelineJob = event['CodePipeline.job'];
const artifact = codePipelineJob.data.inputArtifacts[0];
const s3Bucket = artifact.location.s3Location.bucketName;
const s3Key = artifact.location.s3Location.objectKey;
const versionLabel = `${APPLICATION_NAME}-${Date.now()}`;
const applicationVersionCreated = await createApplicationVersion({
s3Bucket,
s3Key,
versionLabel
});
if (!applicationVersionCreated) {
throw new Error('Could not create application version');
}
To create the application version we use the EB client to send a CreateApplicationVersionCommand to EB. The command accepts the version name, a version description, a version label and the version’s source-bundle info.
The version label consists of a timestamp of the current date appended to the application name.
The source-bundle info, which originates from a prior CodePipeline stage, consists of the name of the S3 bucket in which the bundle is stored and the key of the S3 object representing the bundle.
If the attempt to create the application version fails, the caller throws an error; if it succeeds, the caller is free to continue to the next step in the process: creating the staging environment.
Creating the staging environment
async function createStagingEnvironment({ environmentName, domainName, versionLabel }) {
const cmd = new CreateEnvironmentCommand({
ApplicationName: APPLICATION_NAME,
CNAMEPrefix: domainName,
EnvironmentName: environmentName,
SolutionStackName: SOLUTION_STACK_NAME,
VersionLabel: versionLabel
});
try {
await elasticBeanstalk.send(cmd);
return true;
} catch (err) {
error('Error creating staging environment:', err);
return false;
}
}
const stagingEnvironmentCreated = await createStagingEnvironment({
...environments.next,
versionLabel
});
if (!stagingEnvironmentCreated) {
return await sendPutJobFailureResultCommand(codePipelineJobId, 'Could not create staging environment', context.awsRequestId);
}
To create the staging environment we use the EB client to send a CreateEnvironmentCommand to EB. The command requires the following info:
Key | Value |
---|---|
ApplicationName | APPLICATION_NAME |
CNAMEPrefix | <domainName> |
EnvironmentName | <environmentName> |
SolutionStackName | SOLUTION_STACK_NAME |
VersionLabel | <versionLabel> |
If the command fails, the caller throws an error; if it succeeds, the caller may continue to the final step in the process: waiting for the environment to be ready—it can take several minutes for EB to spin up an environment.
Waiting for the environment to be ready
async function waitForEnvironmentToBeReady({ environmentName, pollInterval = 30, timeout = 600 }) {
const start = Date.now();
let environmentIsReady = false;
while (!environmentIsReady) {
const environments = await describeEnvironments([environmentName]);
// failure: no environments
if (!environments || environments.length === 0) {
error(`Could not describe environments while waiting for environment ${environmentName} to be ready.`);
break;
}
const environment = environments[0];
// success: environment is ready and healthy
if (environment.Status === "Ready" && environment.Health === "Green") {
log(`Environment ${environmentName} is ready and healthy.`);
environmentIsReady = true;
break;
}
// failure: timeout
if ((Date.now() - start) / 1000 > timeout) {
error(`Timeout waiting for environment ${environmentName} to be ready.`);
break;
}
log(`Waiting for environment ${environmentName}... Status: ${environment?.Status}, Health: ${environment?.Health}`);
// neither success nor failure: poll
await new Promise(resolve => setTimeout(resolve, pollInterval * 1000));
}
return environmentIsReady;
}
await waitForEnvironmentToBeReady({environmentName: environments.next.environmentName});
Waiting for the environment to be ready involves polling EB at regular intervals to check for a healthy environment. The polling mechanism takes the form of a while loop whose iterations are controlled by a delay corresponding to a configured polling interval. The delay is implemented as a Promise that is resolved after the polling interval has been reached.
To test for the environment’s readiness we use the EB client to send a DescribeEnvironmentsCommand to EB. If an environment is found and has the appropriate status (Ready) and health (Green), the environment is deemed to be ready, upon which determination a flag (environmentIsReady) is returned to the caller with a value of true. If an environment is not found or a configured timeout is reached, the environment is deemed not to be ready, upon which determination the flag is returned to the caller with a value of false.
On receiving false, the caller throws an error; on receiving true, the caller deems the process as a whole to have been successful and accordingly sends a success result to CodePipeline.
Sanity-checking the function execution
Executing the CodePipeline job results in the Lambda function being invoked and the staging environment being created. The CloudWatch logs for the Lambda confirm that the function ran as expected:
2025-10-19T22:57:16.968-04:00 INIT_START Runtime Version: nodejs:22.v59 Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:78be964f5868c0d4aba0b80888fefce9cdc59266c5fe738cfb0ddd061e6d5450
2025-10-19T22:57:17.476-04:00 START RequestId: 433b5739-c2dd-487b-932d-da03617cd50f Version: $LATEST
2025-10-19T22:57:18.567-04:00 2025-10-19T22:57:18.567Z 433b5739-c2dd-487b-932d-da03617cd50f DEBUG Live environment info {
"environmentName": "davidsmithweb-prod-2",
"domainName": "davidsmithweb-prod-blue"
}
2025-10-19T22:57:18.587-04:00 2025-10-19T22:57:18.587Z 433b5739-c2dd-487b-932d-da03617cd50f DEBUG Staging environment info {
"environmentName": "davidsmithweb-prod-1",
"domainName": "davidsmithweb-prod-green"
}
2025-10-19T22:57:18.587-04:00 2025-10-19T22:57:18.587Z 433b5739-c2dd-487b-932d-da03617cd50f DEBUG Version label "davidsmithweb-1760914638587"
2025-10-19T22:57:18.912-04:00 2025-10-19T22:57:18.912Z 433b5739-c2dd-487b-932d-da03617cd50f DEBUG Application version created true
2025-10-19T22:57:20.781-04:00 2025-10-19T22:57:20.781Z 433b5739-c2dd-487b-932d-da03617cd50f DEBUG Staging environment created true
2025-10-19T22:57:20.940-04:00 2025-10-19T22:57:20.940Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T22:57:51.148-04:00 2025-10-19T22:57:51.148Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T22:58:21.267-04:00 2025-10-19T22:58:21.267Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T22:58:51.427-04:00 2025-10-19T22:58:51.427Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T22:59:21.580-04:00 2025-10-19T22:59:21.580Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T22:59:51.802-04:00 2025-10-19T22:59:51.802Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T23:00:21.968-04:00 2025-10-19T23:00:21.968Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T23:00:52.125-04:00 2025-10-19T23:00:52.125Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T23:01:22.282-04:00 2025-10-19T23:01:22.282Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T23:01:52.419-04:00 2025-10-19T23:01:52.419Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Waiting for environment davidsmithweb-prod-1... Status: Launching, Health: Grey
2025-10-19T23:02:22.554-04:00 2025-10-19T23:02:22.554Z 433b5739-c2dd-487b-932d-da03617cd50f INFO Environment davidsmithweb-prod-1 is ready and healthy.
2025-10-19T23:02:22.554-04:00 2025-10-19T23:02:22.554Z 433b5739-c2dd-487b-932d-da03617cd50f DEBUG Environment ready true
2025-10-19T23:02:22.554-04:00 2025-10-19T23:02:22.554Z 433b5739-c2dd-487b-932d-da03617cd50f DEBUG Environment created. Sending success result to CodePipeline...
2025-10-19T23:02:22.768-04:00 END RequestId: 433b5739-c2dd-487b-932d-da03617cd50f
2025-10-19T23:02:22.768-04:00 REPORT RequestId: 433b5739-c2dd-487b-932d-da03617cd50f Duration: 305291.64 ms Billed Duration: 305796 ms Memory Size: 128 MB Max Memory Used: 102 MB Init Duration: 503.94 ms
Conclusion
In this post I demonstrated the code behind the Lambda function that implements the blue-green deployment strategy for this blog. In summary the Lambda function…
- Determines the basic details of the staging environment
- Creates the application version
- Creates the environment with the application version
- Waits for the environment to be ready
- Notifies CodePipeline of a successful or an unsuccessful execution
A successful execution results in a staging environment being created in EB without the need for manual intervention on the part of the person performing the deployment.
Shared code
Constants
// the name of the application that's deployed to an environment
const APPLICATION_NAME = 'davidsmithweb';
// the domain name for the staging environment
const DOMAIN_NAME_STAGING = 'davidsmithweb-prod-green';
// valid environment names
const ENVIRONMENT_NAMES = ['davidsmithweb-prod-1', 'davidsmithweb-prod-2'];
// the name of the EB platform
const SOLUTION_STACK_NAME = '64bit Amazon Linux 2 v4.1.2 running Docker';
// the AWS region
const REGION = "us-east-1";
// an instance of the Elastic Beanstalk client
const elasticBeanstalk = new ElasticBeanstalkClient({ region: REGION });
// an instance of the CodePipeline client
const codePipeline = new CodePipelineClient({ region: REGION });
Functions
describeEnvironments
Wraps the DescribeEnvironmentsCommand from @aws-sdk/client-elastic-beanstalk.
async function describeEnvironments(environmentNames) {
const cmd = new DescribeEnvironmentsCommand({
ApplicationName: APPLICATION_NAME,
EnvironmentNames: environmentNames,
IncludeDeleted: false
});
try {
const cmdRes = await elasticBeanstalk.send(cmd);
return cmdRes.Environments;
} catch (err) {
error('Error describing environments', err);
return null;
}
}
sendPutJobSuccessResultCommand
Wraps the PutJobSuccessResultCommand from @aws-sdk/client-codepipeline.
async function sendPutJobSuccessResultCommand(jobId, outputVariables = {}) {
return await codePipeline.send(new PutJobSuccessResultCommand({
jobId,
outputVariables
}));
}
sendPutJobFailureResultCommand
Wraps the PutJobSuccessResultCommand from @aws-sdk/client-codepipeline.
async function sendPutJobFailureResultCommand(jobId, message, externalExecutionId) {
return await codePipeline.send(new PutJobFailureResultCommand({
jobId,
failureDetails: {
type: 'JobFailed',
message: JSON.stringify(message),
externalExecutionId
}
}));
}