Skip to content

Commit 7feb74a

Browse files
authored
Merge pull request #4 from tmshkr/dev
v4.0.1
2 parents 52901a6 + 9477092 commit 7feb74a

File tree

9 files changed

+133
-144
lines changed

9 files changed

+133
-144
lines changed

README.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# blue-green-beanstalk
22

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

55
The action will create the following resources:
66

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

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

13-
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.
13+
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.
1414

15-
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.
15+
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.
1616

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

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

2323
## Terminating Environments
2424

25-
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.
25+
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.
2626

27-
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.
27+
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.
2828

2929
## Usage
3030

@@ -42,32 +42,37 @@ name: Example Deploy Workflow
4242
jobs:
4343
deploy:
4444
runs-on: ubuntu-latest
45+
permissions:
46+
id-token: write
47+
contents: read
4548
steps:
4649
- name: Checkout
4750
uses: actions/checkout@v4
51+
- name: Configure AWS credentials
52+
uses: aws-actions/configure-aws-credentials@v4
53+
with:
54+
role-to-assume: ${{ vars.AWS_ROLE }}
55+
aws-region: ${{ vars.AWS_REGION }}
4856
- name: Generate source bundle
4957
run: zip -r bundle.zip . -x '*.git*'
5058
- name: Deploy
5159
uses: tmshkr/blue-green-beanstalk@v4
5260
with:
5361
app_name: "test-app"
54-
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
55-
aws_region: ${{ vars.AWS_REGION }}
56-
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
5762
blue_env: "my-blue-env"
58-
deploy: true
63+
deploy: true # Must be set to true to deploy
5964
green_env: "my-green-env"
6065
platform_branch_name: "Docker running on 64bit Amazon Linux 2023"
6166
production_cname: "blue-green-beanstalk-prod"
6267
source_bundle: "bundle.zip"
63-
swap_cnames: ${{ github.ref_name == 'main' }}
6468
staging_cname: "blue-green-beanstalk-staging"
69+
swap_cnames: ${{ github.ref_name == 'main' }}
6570
version_description: ${{ github.event.head_commit.message }}
6671
version_label: ${{ github.ref_name }}-${{ github.sha }}
6772
```
6873
6974
### Using a Shared Load Balancer
7075
71-
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.
76+
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.
7277

7378
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.

action.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ inputs:
3131
description: "Name of the green environment."
3232
required: true
3333
option_settings:
34-
description: "Path to a JSON file consisting of an array of option settings to use when creating a new environment."
34+
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."
3535
platform_branch_name:
3636
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."
3737
production_cname:
@@ -48,7 +48,7 @@ inputs:
4848
template_name:
4949
description: "Name of an Elastic Beanstalk configuration template to use when creating a new environment."
5050
terminate_unhealthy_environment:
51-
description: "Whether to terminate an unhealthy target environment."
51+
description: "Whether to terminate an unhealthy target environment. If set to false, the action will fail if the target environment is unhealthy."
5252
default: "true"
5353
update_environment:
5454
description: "Whether to update an existing environment during deployment."
@@ -64,13 +64,13 @@ inputs:
6464
version_label:
6565
description: "Version label to use for the new application version."
6666
wait_for_deployment:
67-
description: "Whether to wait for the deployment or environment creation to complete."
67+
description: "Whether to wait for the deployment to complete."
6868
default: "true"
6969
wait_for_environment:
70-
description: "Whether to wait for the target environment to be ready before deployment."
70+
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."
7171
default: "true"
7272
wait_for_termination:
73-
description: "Whether to wait for an environment to be terminated."
73+
description: "Whether to wait for an environment to be terminated. If set to false, the action will fail if the target environment is terminating."
7474
default: "true"
7575
outputs:
7676
target_env_cname:

dist/index.js

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -233998,7 +233998,7 @@ function setDescribeEventsInterval(environmentId, startTime = new Date()) {
233998233998
if (Events.length > 0) {
233999233999
startTime = Events[0].EventDate;
234000234000
for (const e of Events.reverse()) {
234001-
console.log(`${printUTCTime(e.EventDate)} ${e.Severity} ${e.EnvironmentName}: ${e.Message}`);
234001+
console.log(`[${e.EnvironmentName}]: ${printUTCTime(e.EventDate)} ${e.Severity} ${e.Message}`);
234002234002
if (e.Severity === "ERROR") {
234003234003
throw new Error(e.Message);
234004234004
}
@@ -234067,8 +234067,8 @@ var updateListenerRules_awaiter = (undefined && undefined.__awaiter) || function
234067234067

234068234068
function removeTargetGroups(inputs) {
234069234069
return updateListenerRules_awaiter(this, void 0, void 0, function* () {
234070-
const { prodEnv, stagingEnv } = yield getEnvironments(inputs);
234071-
const environments = [prodEnv, stagingEnv].filter((env) => !!env);
234070+
const { stagingEnv } = yield getEnvironments(inputs);
234071+
const environments = [stagingEnv].filter((env) => !!env);
234072234072
const resources = yield getEnvironmentResources(environments);
234073234073
const rules = yield getRules(resources);
234074234074
const { TagDescriptions } = yield elbv2Client.send(new client_elastic_load_balancing_v2_dist_cjs.DescribeTagsCommand({
@@ -234094,7 +234094,7 @@ function removeTargetGroups(inputs) {
234094234094
RuleArn: ResourceArn,
234095234095
Actions: handleActions(rule.Actions),
234096234096
}));
234097-
console.log(`Updated ${inputs.stagingCNAME} rule: ${ResourceArn}`);
234097+
console.log(`Updated rule:`, rule.RuleArn);
234098234098
}
234099234099
}
234100234100
}
@@ -234103,8 +234103,26 @@ function removeTargetGroups(inputs) {
234103234103
function updateTargetGroups(inputs) {
234104234104
var _a, _b, _c;
234105234105
return updateListenerRules_awaiter(this, void 0, void 0, function* () {
234106+
console.log("Updating listener rules...");
234106234107
const { prodEnv, stagingEnv } = yield getEnvironments(inputs);
234107-
const environments = [prodEnv, stagingEnv].filter((env) => !!env);
234108+
const environments = [prodEnv, stagingEnv].filter((env) => {
234109+
if (!env)
234110+
return false;
234111+
if (env.Status !== "Ready") {
234112+
console.log(`[${env.EnvironmentName}]: Status is ${env.Status}`);
234113+
console.log(`[${env.EnvironmentName}]: Skipping...`);
234114+
return false;
234115+
}
234116+
if (env.Health === "Green") {
234117+
return true;
234118+
}
234119+
console.warn(`[${env.EnvironmentName}]: Health is ${env.Health}`);
234120+
if (env.Health === "Yellow") {
234121+
return true;
234122+
}
234123+
console.log(`[${env.EnvironmentName}]: Skipping...`);
234124+
return false;
234125+
});
234108234126
const resources = yield getEnvironmentResources(environments);
234109234127
const rules = yield getRules(resources);
234110234128
const targetGroupARNs = yield findTargetGroupArns(inputs, environments, resources);
@@ -234131,10 +234149,10 @@ function updateTargetGroups(inputs) {
234131234149
RuleArn: ResourceArn,
234132234150
Actions: handleActions(rule.Actions, targetGroupArn),
234133234151
}));
234134-
console.log(`Updated ${cname} rule: ${ResourceArn}`);
234152+
console.log(`Updated rule:`, rule.RuleArn);
234135234153
}
234136234154
else {
234137-
console.warn(`No target group found for ${cname} from ${ResourceArn}`);
234155+
console.warn(`No target group available for ${cname} on rule:`, rule.RuleArn);
234138234156
}
234139234157
}
234140234158
});
@@ -234168,7 +234186,7 @@ function findTargetGroupArns(inputs, environments, resources) {
234168234186
TargetGroupArns: Array.from(targetGroupARNs),
234169234187
}))
234170234188
.then(({ TargetGroups }) => {
234171-
for (const { TargetGroupArn, Port } of TargetGroups) {
234189+
for (const { Port, TargetGroupArn } of TargetGroups) {
234172234190
result[CNAME][Port] = TargetGroupArn;
234173234191
}
234174234192
});
@@ -234215,14 +234233,11 @@ function getRules(resources) {
234215234233
if (loadBalancerArns.size > 1) {
234216234234
throw new Error("Environments must use the same load balancer");
234217234235
}
234218-
const loadBalancerArn = Array.from(loadBalancerArns)[0];
234219-
const listeners = [];
234220-
yield elbv2Client.send(new client_elastic_load_balancing_v2_dist_cjs.DescribeListenersCommand({
234221-
LoadBalancerArn: loadBalancerArn,
234222-
}))
234223-
.then(({ Listeners }) => listeners.push(...Listeners));
234236+
const { Listeners } = yield elbv2Client.send(new client_elastic_load_balancing_v2_dist_cjs.DescribeListenersCommand({
234237+
LoadBalancerArn: Array.from(loadBalancerArns)[0],
234238+
}));
234224234239
const rules = [];
234225-
for (const { ListenerArn } of listeners) {
234240+
for (const { ListenerArn } of Listeners) {
234226234241
yield elbv2Client.send(new client_elastic_load_balancing_v2_dist_cjs.DescribeRulesCommand({ ListenerArn: ListenerArn }))
234227234242
.then(({ Rules }) => {
234228234243
for (const rule of Rules) {
@@ -234256,11 +234271,7 @@ var terminateEnvironment_awaiter = (undefined && undefined.__awaiter) || functio
234256234271
function terminateEnvironment(inputs, env) {
234257234272
return terminateEnvironment_awaiter(this, void 0, void 0, function* () {
234258234273
if (!inputs.terminateUnhealthyEnvironment) {
234259-
throw {
234260-
type: "EarlyExit",
234261-
message: "Target environment is unhealthy and terminateUnhealthyEnvironment is false. Exiting...",
234262-
targetEnv: env,
234263-
};
234274+
throw new Error("Target environment is unhealthy and terminate_unhealthy_environment is false. Exiting...");
234264234275
}
234265234276
if (inputs.updateListenerRules) {
234266234277
yield removeTargetGroups(inputs);
@@ -234279,11 +234290,7 @@ function terminateEnvironment(inputs, env) {
234279234290
clearInterval(interval);
234280234291
}
234281234292
else
234282-
throw {
234283-
type: "EarlyExit",
234284-
message: "Target environment is terminating and waitForTermination is false. Exiting...",
234285-
targetEnv: env,
234286-
};
234293+
throw new Error("Target environment is terminating and wait_for_termination is false. Exiting...");
234287234294
});
234288234295
}
234289234296

@@ -234319,11 +234326,7 @@ function getTargetEnv(inputs) {
234319234326
return null;
234320234327
}
234321234328
else
234322-
throw {
234323-
type: "EarlyExit",
234324-
message: "Target environment is terminating and waitForTermination is false. Exiting...",
234325-
targetEnv,
234326-
};
234329+
throw new Error("Target environment is terminating and wait_for_termination is false. Exiting...");
234327234330
}
234328234331
else if (targetEnv.Status !== "Ready") {
234329234332
if (inputs.waitForEnvironment) {
@@ -234334,11 +234337,7 @@ function getTargetEnv(inputs) {
234334234337
return getTargetEnv(inputs);
234335234338
}
234336234339
else
234337-
throw {
234338-
type: "EarlyExit",
234339-
message: "Target environment is not ready and waitForEnvironment is false. Exiting...",
234340-
targetEnv,
234341-
};
234340+
throw new Error("Target environment is not ready and wait_for_environment is false. Exiting...");
234342234341
}
234343234342
switch (targetEnv.Health) {
234344234343
case "Green":
@@ -234562,14 +234561,8 @@ function main(inputs) {
234562234561
}
234563234562
}
234564234563
catch (err) {
234565-
if (err.type === "EarlyExit") {
234566-
console.log(err.message);
234567-
targetEnv = err.targetEnv;
234568-
}
234569-
else {
234570-
core.setFailed(err.message);
234571-
return Promise.reject(err);
234572-
}
234564+
core.setFailed(err.message);
234565+
return Promise.reject(err);
234573234566
}
234574234567
yield setOutputs(targetEnv);
234575234568
});

src/getTargetEnv.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,9 @@ export async function getTargetEnv(
3131
clearInterval(interval);
3232
return null;
3333
} else
34-
throw {
35-
type: "EarlyExit",
36-
message:
37-
"Target environment is terminating and waitForTermination is false. Exiting...",
38-
targetEnv,
39-
};
34+
throw new Error(
35+
"Target environment is terminating and wait_for_termination is false. Exiting..."
36+
);
4037
} else if (targetEnv.Status !== "Ready") {
4138
if (inputs.waitForEnvironment) {
4239
console.log("Target environment is not ready. Waiting...");
@@ -48,12 +45,9 @@ export async function getTargetEnv(
4845
clearInterval(interval);
4946
return getTargetEnv(inputs);
5047
} else
51-
throw {
52-
type: "EarlyExit",
53-
message:
54-
"Target environment is not ready and waitForEnvironment is false. Exiting...",
55-
targetEnv,
56-
};
48+
throw new Error(
49+
"Target environment is not ready and wait_for_environment is false. Exiting..."
50+
);
5751
}
5852

5953
switch (targetEnv.Health) {

src/main.test.ts

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -145,48 +145,36 @@ describe("main test", () => {
145145

146146
describe("terminate_unhealthy_environment", () => {
147147
it("should not terminate the environment when terminate_unhealthy_environment is set to false", async () => {
148-
await main({
149-
...inputs,
150-
terminateUnhealthyEnvironment: false,
151-
deploy: false,
152-
});
153-
154-
const stagingEnv = await ebClient
155-
.send(
156-
new DescribeEnvironmentsCommand({
157-
ApplicationName: inputs.appName,
158-
EnvironmentNames: [inputs.blueEnv, inputs.greenEnv],
159-
})
160-
)
161-
.then(({ Environments }) =>
162-
Environments.find((env) => env.CNAME === stagingDomain)
148+
try {
149+
await main({
150+
...inputs,
151+
terminateUnhealthyEnvironment: false,
152+
deploy: false,
153+
});
154+
throw new Error("Should not reach here");
155+
} catch (err) {
156+
expect(err.message).toEqual(
157+
"Target environment is unhealthy and terminate_unhealthy_environment is false. Exiting..."
163158
);
164-
165-
expect(stagingEnv.Health).toEqual("Grey");
159+
}
166160
});
167161
});
168162

169163
describe("wait_for_termination", () => {
170164
it("should not wait for the environment to terminate when wait_for_termination is set to false", async () => {
171-
await main({
172-
...inputs,
173-
terminateUnhealthyEnvironment: true,
174-
waitForTermination: false,
175-
deploy: false,
176-
});
177-
178-
const stagingEnv = await ebClient
179-
.send(
180-
new DescribeEnvironmentsCommand({
181-
ApplicationName: inputs.appName,
182-
EnvironmentNames: [inputs.blueEnv, inputs.greenEnv],
183-
})
184-
)
185-
.then(({ Environments }) =>
186-
Environments.find((env) => env.CNAME === stagingDomain)
165+
try {
166+
await main({
167+
...inputs,
168+
terminateUnhealthyEnvironment: true,
169+
waitForTermination: false,
170+
deploy: false,
171+
});
172+
throw new Error("Should not reach here");
173+
} catch (err) {
174+
expect(err.message).toEqual(
175+
"Target environment is terminating and wait_for_termination is false. Exiting..."
187176
);
188-
189-
expect(stagingEnv.Status).toEqual("Terminating");
177+
}
190178
});
191179
});
192180
});

src/main.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,8 @@ export async function main(inputs: ActionInputs) {
4040
await updateTargetGroups(inputs);
4141
}
4242
} catch (err) {
43-
if (err.type === "EarlyExit") {
44-
console.log(err.message);
45-
targetEnv = err.targetEnv;
46-
} else {
47-
core.setFailed(err.message);
48-
return Promise.reject(err);
49-
}
43+
core.setFailed(err.message);
44+
return Promise.reject(err);
5045
}
5146

5247
await setOutputs(targetEnv);

0 commit comments

Comments
 (0)