Skip to content

Commit

Permalink
Merge pull request #4 from tmshkr/dev
Browse files Browse the repository at this point in the history
v4.0.1
  • Loading branch information
tmshkr authored Jan 4, 2024
2 parents 52901a6 + 9477092 commit 7feb74a
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 144 deletions.
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# blue-green-beanstalk

GitHub Action to automate deployment to blue/green environments on AWS Elastic Beanstalk.
GitHub Action to automate [blue/green deployment](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features.CNAMESwap.html) with AWS Elastic Beanstalk.

The action will create the following resources:

Expand All @@ -10,9 +10,9 @@ The action will create the following resources:

Based on the provided inputs, the action will determine which environment is the target environment, to which a new application version should be deployed.

The action uses the values of the `production_cname` and `staging_cname` inputs to determine which environment is the production or staging environment. Accordingly, the `production_cname` and `staging_cname` inputs should be set to the CNAME prefix of the production and staging environments, respectively.
The action uses the values of the `production_cname` and `staging_cname` inputs to determine which environment is the production or staging environment. Accordingly, the production CNAME should always point to the production environment, and staging CNAME should always point to the staging environment.

If neither environment exists, it will create a new environment with the `production_cname` input. If the production environment already exists, the action will target the staging environment, creating it if it doesn't exist.
If neither environment exists, the action will create a new environment with the `production_cname` input. If the production environment already exists, the action will target the staging environment, creating it if it doesn't exist.

After deploying, the action will swap the CNAMEs of the staging and production environments, if `swap_cnames` is set to true.

Expand All @@ -22,9 +22,9 @@ See [action.yml](action.yml)

## Terminating Environments

If the action finds that the target environment is in an unhealthy state, it will be terminated and recreated, unless `terminate_unhealthy_environment` is set to false. The environment should be configured to recreate any associated resources that are deleted during environment termination, so that they are available when it is recreated.
If the action finds that the staging environment is in an unhealthy state, it will be terminated and recreated, unless `terminate_unhealthy_environment` is set to false. The environment should be configured to recreate any associated resources that are deleted during environment termination, so that they are available when it is recreated.

The action will also enable or disable termination protection on the target environment's underlying CloudFormation stack, if `enable_termination_protection` or `disable_termination_protection` are set to true, respectively.
Termination protection can be enabled or disabled on the target environment's underlying CloudFormation stack by setting `enable_termination_protection` or `disable_termination_protection` to true.

## Usage

Expand All @@ -42,32 +42,37 @@ name: Example Deploy Workflow
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
- name: Generate source bundle
run: zip -r bundle.zip . -x '*.git*'
- name: Deploy
uses: tmshkr/blue-green-beanstalk@v4
with:
app_name: "test-app"
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_region: ${{ vars.AWS_REGION }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
blue_env: "my-blue-env"
deploy: true
deploy: true # Must be set to true to deploy
green_env: "my-green-env"
platform_branch_name: "Docker running on 64bit Amazon Linux 2023"
production_cname: "blue-green-beanstalk-prod"
source_bundle: "bundle.zip"
swap_cnames: ${{ github.ref_name == 'main' }}
staging_cname: "blue-green-beanstalk-staging"
swap_cnames: ${{ github.ref_name == 'main' }}
version_description: ${{ github.event.head_commit.message }}
version_label: ${{ github.ref_name }}-${{ github.sha }}
```
### Using a Shared Load Balancer
When using a [shared load balancer](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/environments-cfg-alb-shared.html), the `update_listener_rules` input can be set to true, so that the action will update any listener rules that are tagged with a `bluegreenbeanstalk:target_cname` key, whose value is equal to the `production_cname` or `staging_cname` input, so that the listener rule will be updated to point to the same target group as the CNAME.
When using a [shared load balancer](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/environments-cfg-alb-shared.html), the `update_listener_rules` input can be set to true, and the action will update any listener rules on the load balancer that are tagged with a `bluegreenbeanstalk:target_cname` key, whose value is equal to the `production_cname` or `staging_cname` input, so that the listener rule points to the same target group as the CNAME.

If using a process on a port besides the default port 80, set another tag on the listener rule with a `bluegreenbeanstalk:target_port` key and a value equal to the port number, so that the listener rule forwards to the target group on that port.
10 changes: 5 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ inputs:
description: "Name of the green environment."
required: true
option_settings:
description: "Path to a JSON file consisting of an array of option settings to use when creating a new environment."
description: "Path to a JSON file consisting of an array of option settings to use when updating an existing evironment or creating a new environment."
platform_branch_name:
description: "Name of the platform branch to use. When creating a new environment, it will be launched with the latest version of the specified platform branch. To see the list of available platform branches, run the `aws elasticbeanstalk list-platform-branches` command."
production_cname:
Expand All @@ -48,7 +48,7 @@ inputs:
template_name:
description: "Name of an Elastic Beanstalk configuration template to use when creating a new environment."
terminate_unhealthy_environment:
description: "Whether to terminate an unhealthy target environment."
description: "Whether to terminate an unhealthy target environment. If set to false, the action will fail if the target environment is unhealthy."
default: "true"
update_environment:
description: "Whether to update an existing environment during deployment."
Expand All @@ -64,13 +64,13 @@ inputs:
version_label:
description: "Version label to use for the new application version."
wait_for_deployment:
description: "Whether to wait for the deployment or environment creation to complete."
description: "Whether to wait for the deployment to complete."
default: "true"
wait_for_environment:
description: "Whether to wait for the target environment to be ready before deployment."
description: "Whether to wait for the target environment to be ready before deployment. If set to false, the action will fail if the target environment is not ready."
default: "true"
wait_for_termination:
description: "Whether to wait for an environment to be terminated."
description: "Whether to wait for an environment to be terminated. If set to false, the action will fail if the target environment is terminating."
default: "true"
outputs:
target_env_cname:
Expand Down
79 changes: 36 additions & 43 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -233998,7 +233998,7 @@ function setDescribeEventsInterval(environmentId, startTime = new Date()) {
if (Events.length > 0) {
startTime = Events[0].EventDate;
for (const e of Events.reverse()) {
console.log(`${printUTCTime(e.EventDate)} ${e.Severity} ${e.EnvironmentName}: ${e.Message}`);
console.log(`[${e.EnvironmentName}]: ${printUTCTime(e.EventDate)} ${e.Severity} ${e.Message}`);
if (e.Severity === "ERROR") {
throw new Error(e.Message);
}
Expand Down Expand Up @@ -234067,8 +234067,8 @@ var updateListenerRules_awaiter = (undefined && undefined.__awaiter) || function

function removeTargetGroups(inputs) {
return updateListenerRules_awaiter(this, void 0, void 0, function* () {
const { prodEnv, stagingEnv } = yield getEnvironments(inputs);
const environments = [prodEnv, stagingEnv].filter((env) => !!env);
const { stagingEnv } = yield getEnvironments(inputs);
const environments = [stagingEnv].filter((env) => !!env);
const resources = yield getEnvironmentResources(environments);
const rules = yield getRules(resources);
const { TagDescriptions } = yield elbv2Client.send(new client_elastic_load_balancing_v2_dist_cjs.DescribeTagsCommand({
Expand All @@ -234094,7 +234094,7 @@ function removeTargetGroups(inputs) {
RuleArn: ResourceArn,
Actions: handleActions(rule.Actions),
}));
console.log(`Updated ${inputs.stagingCNAME} rule: ${ResourceArn}`);
console.log(`Updated rule:`, rule.RuleArn);
}
}
}
Expand All @@ -234103,8 +234103,26 @@ function removeTargetGroups(inputs) {
function updateTargetGroups(inputs) {
var _a, _b, _c;
return updateListenerRules_awaiter(this, void 0, void 0, function* () {
console.log("Updating listener rules...");
const { prodEnv, stagingEnv } = yield getEnvironments(inputs);
const environments = [prodEnv, stagingEnv].filter((env) => !!env);
const environments = [prodEnv, stagingEnv].filter((env) => {
if (!env)
return false;
if (env.Status !== "Ready") {
console.log(`[${env.EnvironmentName}]: Status is ${env.Status}`);
console.log(`[${env.EnvironmentName}]: Skipping...`);
return false;
}
if (env.Health === "Green") {
return true;
}
console.warn(`[${env.EnvironmentName}]: Health is ${env.Health}`);
if (env.Health === "Yellow") {
return true;
}
console.log(`[${env.EnvironmentName}]: Skipping...`);
return false;
});
const resources = yield getEnvironmentResources(environments);
const rules = yield getRules(resources);
const targetGroupARNs = yield findTargetGroupArns(inputs, environments, resources);
Expand All @@ -234131,10 +234149,10 @@ function updateTargetGroups(inputs) {
RuleArn: ResourceArn,
Actions: handleActions(rule.Actions, targetGroupArn),
}));
console.log(`Updated ${cname} rule: ${ResourceArn}`);
console.log(`Updated rule:`, rule.RuleArn);
}
else {
console.warn(`No target group found for ${cname} from ${ResourceArn}`);
console.warn(`No target group available for ${cname} on rule:`, rule.RuleArn);
}
}
});
Expand Down Expand Up @@ -234168,7 +234186,7 @@ function findTargetGroupArns(inputs, environments, resources) {
TargetGroupArns: Array.from(targetGroupARNs),
}))
.then(({ TargetGroups }) => {
for (const { TargetGroupArn, Port } of TargetGroups) {
for (const { Port, TargetGroupArn } of TargetGroups) {
result[CNAME][Port] = TargetGroupArn;
}
});
Expand Down Expand Up @@ -234215,14 +234233,11 @@ function getRules(resources) {
if (loadBalancerArns.size > 1) {
throw new Error("Environments must use the same load balancer");
}
const loadBalancerArn = Array.from(loadBalancerArns)[0];
const listeners = [];
yield elbv2Client.send(new client_elastic_load_balancing_v2_dist_cjs.DescribeListenersCommand({
LoadBalancerArn: loadBalancerArn,
}))
.then(({ Listeners }) => listeners.push(...Listeners));
const { Listeners } = yield elbv2Client.send(new client_elastic_load_balancing_v2_dist_cjs.DescribeListenersCommand({
LoadBalancerArn: Array.from(loadBalancerArns)[0],
}));
const rules = [];
for (const { ListenerArn } of listeners) {
for (const { ListenerArn } of Listeners) {
yield elbv2Client.send(new client_elastic_load_balancing_v2_dist_cjs.DescribeRulesCommand({ ListenerArn: ListenerArn }))
.then(({ Rules }) => {
for (const rule of Rules) {
Expand Down Expand Up @@ -234256,11 +234271,7 @@ var terminateEnvironment_awaiter = (undefined && undefined.__awaiter) || functio
function terminateEnvironment(inputs, env) {
return terminateEnvironment_awaiter(this, void 0, void 0, function* () {
if (!inputs.terminateUnhealthyEnvironment) {
throw {
type: "EarlyExit",
message: "Target environment is unhealthy and terminateUnhealthyEnvironment is false. Exiting...",
targetEnv: env,
};
throw new Error("Target environment is unhealthy and terminate_unhealthy_environment is false. Exiting...");
}
if (inputs.updateListenerRules) {
yield removeTargetGroups(inputs);
Expand All @@ -234279,11 +234290,7 @@ function terminateEnvironment(inputs, env) {
clearInterval(interval);
}
else
throw {
type: "EarlyExit",
message: "Target environment is terminating and waitForTermination is false. Exiting...",
targetEnv: env,
};
throw new Error("Target environment is terminating and wait_for_termination is false. Exiting...");
});
}

Expand Down Expand Up @@ -234319,11 +234326,7 @@ function getTargetEnv(inputs) {
return null;
}
else
throw {
type: "EarlyExit",
message: "Target environment is terminating and waitForTermination is false. Exiting...",
targetEnv,
};
throw new Error("Target environment is terminating and wait_for_termination is false. Exiting...");
}
else if (targetEnv.Status !== "Ready") {
if (inputs.waitForEnvironment) {
Expand All @@ -234334,11 +234337,7 @@ function getTargetEnv(inputs) {
return getTargetEnv(inputs);
}
else
throw {
type: "EarlyExit",
message: "Target environment is not ready and waitForEnvironment is false. Exiting...",
targetEnv,
};
throw new Error("Target environment is not ready and wait_for_environment is false. Exiting...");
}
switch (targetEnv.Health) {
case "Green":
Expand Down Expand Up @@ -234562,14 +234561,8 @@ function main(inputs) {
}
}
catch (err) {
if (err.type === "EarlyExit") {
console.log(err.message);
targetEnv = err.targetEnv;
}
else {
core.setFailed(err.message);
return Promise.reject(err);
}
core.setFailed(err.message);
return Promise.reject(err);
}
yield setOutputs(targetEnv);
});
Expand Down
18 changes: 6 additions & 12 deletions src/getTargetEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,9 @@ export async function getTargetEnv(
clearInterval(interval);
return null;
} else
throw {
type: "EarlyExit",
message:
"Target environment is terminating and waitForTermination is false. Exiting...",
targetEnv,
};
throw new Error(
"Target environment is terminating and wait_for_termination is false. Exiting..."
);
} else if (targetEnv.Status !== "Ready") {
if (inputs.waitForEnvironment) {
console.log("Target environment is not ready. Waiting...");
Expand All @@ -48,12 +45,9 @@ export async function getTargetEnv(
clearInterval(interval);
return getTargetEnv(inputs);
} else
throw {
type: "EarlyExit",
message:
"Target environment is not ready and waitForEnvironment is false. Exiting...",
targetEnv,
};
throw new Error(
"Target environment is not ready and wait_for_environment is false. Exiting..."
);
}

switch (targetEnv.Health) {
Expand Down
58 changes: 23 additions & 35 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,48 +145,36 @@ describe("main test", () => {

describe("terminate_unhealthy_environment", () => {
it("should not terminate the environment when terminate_unhealthy_environment is set to false", async () => {
await main({
...inputs,
terminateUnhealthyEnvironment: false,
deploy: false,
});

const stagingEnv = await ebClient
.send(
new DescribeEnvironmentsCommand({
ApplicationName: inputs.appName,
EnvironmentNames: [inputs.blueEnv, inputs.greenEnv],
})
)
.then(({ Environments }) =>
Environments.find((env) => env.CNAME === stagingDomain)
try {
await main({
...inputs,
terminateUnhealthyEnvironment: false,
deploy: false,
});
throw new Error("Should not reach here");
} catch (err) {
expect(err.message).toEqual(
"Target environment is unhealthy and terminate_unhealthy_environment is false. Exiting..."
);

expect(stagingEnv.Health).toEqual("Grey");
}
});
});

describe("wait_for_termination", () => {
it("should not wait for the environment to terminate when wait_for_termination is set to false", async () => {
await main({
...inputs,
terminateUnhealthyEnvironment: true,
waitForTermination: false,
deploy: false,
});

const stagingEnv = await ebClient
.send(
new DescribeEnvironmentsCommand({
ApplicationName: inputs.appName,
EnvironmentNames: [inputs.blueEnv, inputs.greenEnv],
})
)
.then(({ Environments }) =>
Environments.find((env) => env.CNAME === stagingDomain)
try {
await main({
...inputs,
terminateUnhealthyEnvironment: true,
waitForTermination: false,
deploy: false,
});
throw new Error("Should not reach here");
} catch (err) {
expect(err.message).toEqual(
"Target environment is terminating and wait_for_termination is false. Exiting..."
);

expect(stagingEnv.Status).toEqual("Terminating");
}
});
});
});
Expand Down
9 changes: 2 additions & 7 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,8 @@ export async function main(inputs: ActionInputs) {
await updateTargetGroups(inputs);
}
} catch (err) {
if (err.type === "EarlyExit") {
console.log(err.message);
targetEnv = err.targetEnv;
} else {
core.setFailed(err.message);
return Promise.reject(err);
}
core.setFailed(err.message);
return Promise.reject(err);
}

await setOutputs(targetEnv);
Expand Down
Loading

0 comments on commit 7feb74a

Please sign in to comment.