Skip to content

Commit 5274d11

Browse files
authored
Merge branch 'master' into feature/387
2 parents 40cc719 + 5598044 commit 5274d11

12 files changed

Lines changed: 276 additions & 44 deletions

File tree

docs/admin-guide.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,18 +320,37 @@ pipelines:
320320
```
321321

322322
## Service Control Policies
323-
Service control policies *(SCPs)* are one type of policy that you can use to manage your organization. SCPs offer central control over the maximum available permissions for all accounts in your organization, allowing you to ensure your accounts stay within your organization’s access control guidelines. ADF allows SCPs to be applied in a similar fashion as base stacks. You can define your SCP definition in a file named `scp.json` and place it in a folder that represents your Organizational Unit within the `adf-bootstrap` folder from the `aws-deployment-framework-bootstrap` repository on the Master Account.
323+
Service control policies *(SCPs)* are one type of policy that you can use to manage your organization. SCPs offer central control over the maximum available permissions for all accounts in your organization, allowing you to ensure your accounts stay within your organization’s access control guidelines. ADF allows SCPs to be applied in a similar fashion as base stacks. You can define your SCP definition in a file named `scp.json` and place it in a folder that represents your Organizational Unit (or OU/AccountName path if you are wanting to apply an account-specific SCP) within the `adf-bootstrap` folder from the `aws-deployment-framework-bootstrap` repository on the Master Account.
324+
325+
For example, if you have an account named `my_banking_account` under the `banking/dev` OU that needs a specific SCP, and another SCP defined for the whole `deployment` OU, the folder structure would look like this:
326+
327+
```
328+
adf-bootstrap <-- This folder lives in the aws-deployment-framework-bootstrap repository on the master account.
329+
330+
└─── deployment
331+
│ |
332+
│ └─── scp.json
333+
334+
│───banking
335+
│ │
336+
│ └─── dev
337+
│ │
338+
│ └─── my_banking_account
339+
│ └─── scp.json
340+
341+
```
342+
The file `adf-bootstrap/deployment/scp.json` applies the defined SCP to the `deployment` *OU*, while the file `adf-bootstrap/banking/dev/my_backing_account/scp.json` applies the defined SCP to the `my_banking_account` *account*.
324343

325344
SCPs are available only in an organization that has [all features enabled](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_org_support-all-features.html). Once you have enabled all features within your Organization, ADF can manage and automate the application and updating process of the SCPs.
326345

327346
## Tagging Policies
328-
Tag Policies are a feature that allows you to define rules on how tags can be used on AWS resources in your accounts in AWS Organizations. You can use Tag Policies to easily adopt a standardized approach for tagging AWS resources. You can define your Tagging Policy definition in a file named `tagging-policy.json` and place it in a folder that represents your Organizational Unit within the `adf-bootstrap` folder from the `aws-deployment-framework-bootstrap` repository on the Master Account.
347+
Tag Policies are a feature that allows you to define rules on how tags can be used on AWS resources in your accounts in AWS Organizations. You can use Tag Policies to easily adopt a standardized approach for tagging AWS resources. You can define your Tagging Policy definition in a file named `tagging-policy.json` and place it in a folder that represents your Organizational Unit within the `adf-bootstrap` folder from the `aws-deployment-framework-bootstrap` repository on the Master Account. Tagging policies can also be applied to single account using the same approach described above for SCPs.
329348

330349
Tag Policies are available only in an organization that has [all features enabled](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_org_support-all-features.html). Once you have enabled all features within your Organization, ADF can manage and automate the application and updating process of the Tag Policies. For more information, see [here](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_tag-policies.html).
331350

332351

333352
## Integrating Slack
334-
353+
### Integrating with Slack using Lambda
335354
The ADF allows alternate *notification_endpoint* values that can be used to notify the status of a specific pipeline *(in deployment_map.yml)*. You can specify an email address in the deployment map and notifications will be emailed directly to that address. However, if you specify a slack channel name *(eg team-bugs)* as the value, the notifications will be forwarded to that channel. In order to setup this integration you will need to create a [Slack App](https://api.slack.com/apps). When you create your Slack app, you can create multiple Webhook URL's *(Incoming Webhook)* that are each associated with their own channel. Create a webhook for each channel you plan on using throughout your Organization. Once created, copy the webhook URL and create a new secret in Secrets Manager on the Deployment Account:
336355

337356
1. In AWS Console, click _Store a new secret_ and select type 'Other type of secrets' *(eg API Key)*.
@@ -362,6 +381,22 @@ pipelines:
362381
name: omg_production
363382
```
364383

384+
### Integrating with Slack with AWS ChatBot
385+
The ADF also supports integrating pipeline notifications with Slack via the AWS ChatBot. This allows pipeline notifications to scale and provides a consistent Slack notification across different AWS services.
386+
387+
In order to use AWS ChatBot, first you must configure an (AWS ChatBot Client)[https://us-east-2.console.aws.amazon.com/chatbot/home?region=eu-west-1#/chat-clients] for your desired Slack workspace. Once the client has been created. You will need to manually create a channel configuration that will be used by the ADF.
388+
389+
Currently, dynamically creating channel configurations is not supported. In the deployment map, you can configure a unique channel via the notification endpoint parameter for each pipeline separately. Add the `params` section if that is missing and add the following configuration to the pipeline:
390+
```
391+
pipelines:
392+
- name: some-pipeline
393+
# ...
394+
params:
395+
notification_endpoint:
396+
type: chat_bot
397+
target: my_channel_config
398+
```
399+
365400
## Check Current Version
366401

367402
To determine the current version, follow these steps:

docs/user-guide.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,16 @@ Pipelines also have parameters that don't relate to a specific stage but rather
195195

196196
The following are the available pipeline parameters:
197197

198-
- *notification_endpoint* *(String)* defaults to none.
199-
> Can either be a valid email address or a string that represents the name of a Slack Channel. In order to integrate ADF with Slack see [Integrating with Slack](./admin-guide.md) in the admin guide. By Default, Notifications will be sent when pipelines Start, Complete or Fail.
198+
- *notification_endpoint* *(String) | (Dict) * defaults to none.
199+
> Can either be a valid email address or a string that represents the name of a Slack Channel.
200+
> A more complex configuration can be provided to integrate with Slack via AWS ChatBot.
201+
> ```yaml
202+
> notification_endpoint:
203+
> type: chat_bot
204+
> target: example_slack_channel # This is the name of an slack channel configuration you created within the AWS Chat Bot service. This needs to be created before you apply the changes to the deployment map.
205+
> ```
206+
>
207+
> In order to integrate ADF with Slack see [Integrating with Slack](./admin-guide.md#integrating-with-slack-with-aws-chatbot) in the admin guide. By default, notifications will be sent when pipelines Start, Complete, or Fail.
200208

201209
- *schedule* *(String)* defaults to none.
202210
> If the Pipeline should execute on a specific Schedule. Schedules are defined by using a Rate or an Expression. See [here](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#RateExpressions) for more information on how to define Rate or an Expression.

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/configparser.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,41 @@
88

99
import os
1010
import yaml
11+
from logger import configure_logger
1112
from .account import Account
1213

1314

15+
LOGGER = configure_logger(__name__)
16+
17+
1418
def read_config_files(folder):
1519
files = [os.path.join(folder, f) for f in os.listdir(folder)]
1620
accounts = []
1721
for filename in files:
18-
if filename.endswith(".yml"):
19-
with open(filename, 'r') as stream:
20-
config = yaml.safe_load(stream)
21-
for account in config.get('accounts', []):
22-
accounts.append(Account.load_from_config(account))
22+
if not filename.endswith(".yml"):
23+
# Skipping files that do not end with .yml
24+
continue
25+
accounts.extend(_read_config_file(filename))
2326

2427
return accounts
28+
29+
30+
def _read_config_file(filename):
31+
accounts = []
32+
try:
33+
with open(filename, 'r') as stream:
34+
config = yaml.safe_load(stream)
35+
for account in config.get('accounts', []):
36+
accounts.append(Account.load_from_config(account))
37+
return accounts
38+
except Exception as error:
39+
LOGGER.error(
40+
"Could not process %s due to an error: %s. ",
41+
filename,
42+
error
43+
)
44+
LOGGER.error(
45+
"Make sure the content of YAML files (.yml) are not empty and "
46+
"contain a valid YAML data structure.",
47+
)
48+
raise
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: MIT-0
3+
4+
"""Construct related to Notifications Codepipeline Input
5+
"""
6+
7+
import os
8+
from aws_cdk import (
9+
aws_codestarnotifications as cp_notifications,
10+
aws_codepipeline as codepipeline,
11+
core,
12+
)
13+
from logger import configure_logger
14+
15+
ADF_DEPLOYMENT_REGION = os.environ["AWS_REGION"]
16+
ADF_DEPLOYMENT_ACCOUNT_ID = os.environ["ACCOUNT_ID"]
17+
18+
LOGGER = configure_logger(__name__)
19+
20+
EVENT_TYPE_IDS = [
21+
"codepipeline-pipeline-stage-execution-succeeded",
22+
"codepipeline-pipeline-stage-execution-failed",
23+
"codepipeline-pipeline-pipeline-execution-started",
24+
"codepipeline-pipeline-pipeline-execution-failed",
25+
"codepipeline-pipeline-pipeline-execution-succeeded",
26+
"codepipeline-pipeline-manual-approval-needed",
27+
"codepipeline-pipeline-manual-approval-succeeded",
28+
]
29+
30+
31+
class PipelineNotifications(core.Construct):
32+
def __init__(
33+
self,
34+
scope: core.Construct,
35+
id: str,
36+
pipeline: codepipeline.CfnPipeline,
37+
notification_config,
38+
**kwargs,
39+
): # pylint: disable=W0622
40+
super().__init__(scope, id, **kwargs)
41+
slack_channel_arn = f"arn:aws:chatbot::{ADF_DEPLOYMENT_ACCOUNT_ID}:chat-configuration/slack-channel/{notification_config.get('target')}"
42+
pipeline_arn = f"arn:aws:codepipeline:{ADF_DEPLOYMENT_REGION}:{ADF_DEPLOYMENT_ACCOUNT_ID}:{pipeline.ref}"
43+
cp_notifications.CfnNotificationRule(
44+
scope,
45+
"pipeline-notification",
46+
detail_type="FULL",
47+
event_type_ids=EVENT_TYPE_IDS,
48+
name=pipeline.ref,
49+
resource=pipeline_arn,
50+
targets=[
51+
cp_notifications.CfnNotificationRule.TargetProperty(
52+
target_type=PipelineNotifications.get_target_type_from_config(
53+
scope, notification_config
54+
),
55+
target_address=slack_channel_arn,
56+
)
57+
],
58+
)
59+
60+
@staticmethod
61+
def get_target_type_from_config(scope, config):
62+
target_type = config.get("type", "chat_bot")
63+
if target_type == "chat_bot":
64+
return "AWSChatbotSlack"
65+
scope.node.add_error(
66+
f"{target_type} is not supported for CodePipeline notifications."
67+
)
68+
return None

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_constructs/adf_notifications.py

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121

2222

2323
class Notifications(core.Construct):
24-
def __init__(self, scope: core.Construct, id: str, map_params: dict, **kwargs): #pylint: disable=W0622
24+
def __init__(
25+
self, scope: core.Construct, id: str, map_params: dict, **kwargs
26+
): # pylint: disable=W0622
2527
super().__init__(scope, id, **kwargs)
2628
LOGGER.debug('Notification configuration required for %s', map_params['name'])
2729
stack = core.Stack.of(self)
@@ -32,42 +34,36 @@ def __init__(self, scope: core.Construct, id: str, map_params: dict, **kwargs):
3234
f'arn:{stack.partition}:lambda:{ADF_DEPLOYMENT_REGION}:'
3335
f'{ADF_DEPLOYMENT_ACCOUNT_ID}:function:SendSlackNotification'
3436
)
35-
_topic = _sns.Topic(self, 'PipelineTopic')
37+
_topic = _sns.Topic(self, "PipelineTopic")
3638
_statement = _iam.PolicyStatement(
3739
actions=["sns:Publish"],
3840
effect=_iam.Effect.ALLOW,
3941
principals=[
40-
_iam.ServicePrincipal(
41-
'sns.amazonaws.com'
42-
),
43-
_iam.ServicePrincipal(
44-
'codecommit.amazonaws.com'
45-
),
46-
_iam.ServicePrincipal(
47-
'events.amazonaws.com'
48-
)
42+
_iam.ServicePrincipal("sns.amazonaws.com"),
43+
_iam.ServicePrincipal("codecommit.amazonaws.com"),
44+
_iam.ServicePrincipal("events.amazonaws.com"),
4945
],
50-
resources=["*"]
46+
resources=["*"],
5147
)
5248
_topic.add_to_resource_policy(_statement)
53-
_lambda.CfnPermission(
54-
self,
55-
'slack_notification_sns_permissions',
56-
principal='sns.amazonaws.com',
57-
action='lambda:InvokeFunction',
58-
source_arn=_topic.topic_arn,
59-
function_name='SendSlackNotification'
60-
)
61-
_endpoint = map_params.get('params', {}).get('notification_endpoint', '')
49+
_endpoint = map_params.get("params", {}).get("notification_endpoint", "")
6250
_sub = _sns.Subscription(
6351
self,
64-
'sns_subscription',
52+
"sns_subscription",
6553
topic=_topic,
66-
endpoint=_endpoint if '@' in _endpoint else _slack_func.function_arn,
67-
protocol=_sns.SubscriptionProtocol.EMAIL if '@' in _endpoint else _sns.SubscriptionProtocol.LAMBDA
54+
endpoint=_endpoint if "@" in _endpoint else _slack_func.function_arn,
55+
protocol=_sns.SubscriptionProtocol.EMAIL
56+
if "@" in _endpoint
57+
else _sns.SubscriptionProtocol.LAMBDA,
6858
)
69-
if '@' not in _endpoint:
70-
_slack_func.add_event_source(
71-
source=_event_sources.SnsEventSource(_topic)
59+
if "@" not in _endpoint:
60+
_lambda.CfnPermission(
61+
self,
62+
"slack_notification_sns_permissions",
63+
principal="sns.amazonaws.com",
64+
action="lambda:InvokeFunction",
65+
source_arn=_topic.topic_arn,
66+
function_name="SendSlackNotification",
7267
)
68+
_slack_func.add_event_source(source=_event_sources.SnsEventSource(_topic))
7369
self.topic_arn = _topic.topic_arn

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_stacks/adf_default_pipeline.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from cdk_constructs import adf_s3
1717
from cdk_constructs import adf_cloudformation
1818
from cdk_constructs import adf_notifications
19+
from cdk_constructs import adf_chatbot
1920
from logger import configure_logger
2021

2122
ADF_DEPLOYMENT_REGION = os.environ["AWS_REGION"]
@@ -29,10 +30,10 @@
2930
def generate_adf_default_pipeline(scope: core.Stack, stack_input):
3031
_stages = []
3132

32-
if stack_input["input"].get("params", {}).get("notification_endpoint"):
33-
stack_input["input"]["topic_arn"] = adf_notifications.Notifications(
34-
scope, "adf_notifications", stack_input["input"]
35-
).topic_arn
33+
notification_config = stack_input["input"].get("params", {}).get("notification_endpoint", {})
34+
35+
if isinstance(notification_config, str) or notification_config.get('type', '') == "lambda":
36+
stack_input["input"]["topic_arn"] = adf_notifications.Notifications(scope, "adf_notifications", stack_input["input"]).topic_arn
3637

3738
_source_name = generate_source_stage_for_pipeline(_stages, scope, stack_input)
3839
generate_build_stage_for_pipeline(_stages, scope, stack_input)
@@ -41,9 +42,12 @@ def generate_adf_default_pipeline(scope: core.Stack, stack_input):
4142
_pipeline = adf_codepipeline.Pipeline(
4243
scope, "code_pipeline", stack_input["input"], stack_input["ssm_params"], _stages
4344
)
45+
4446
if "github" in _source_name:
4547
adf_github.GitHub.create_webhook_when_required(scope, _pipeline.cfn, stack_input["input"])
4648

49+
if isinstance(notification_config, dict) and notification_config.get('type', '') == 'chat_bot':
50+
adf_chatbot.PipelineNotifications(scope, "adf_chatbot_notifications", _pipeline.cfn, notification_config)
4751

4852
def generate_source_stage_for_pipeline(_stages, scope, stack_input):
4953
_source_name = stack_input["input"]["default_providers"]["source"][

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_stacks/tests/test_pipeline_creation.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,50 @@ def test_pipeline_creation_outputs_as_expected_when_source_is_codecommit_with_co
194194

195195
assert len(build_stage['Actions']) == 1
196196

197+
198+
def test_pipeline_creation_outputs_as_expected_when_notification_endpoint_is_chatbot():
199+
region_name = "eu-central-1"
200+
acount_id = "123456789012"
201+
202+
stack_input = {
203+
"input": {"params": {"notification_endpoint": {"target": "fake-config", "type": "chat_bot"}}, "default_providers": {}, "regions": {}, },
204+
"ssm_params": {"fake-region": {}},
205+
}
206+
207+
stack_input["input"]["name"] = "test-stack"
208+
209+
stack_input["input"]["default_providers"]["source"] = {
210+
"provider": "codecommit",
211+
"properties": {"account_id": "123456789012"},
212+
}
213+
stack_input["input"]["default_providers"]["build"] = {
214+
"provider": "codebuild",
215+
"properties": {"account_id": "123456789012"},
216+
}
217+
218+
stack_input["ssm_params"][region_name] = {
219+
"modules": "fake-bucket-name",
220+
"kms": f"arn:aws:kms:{region_name}:{acount_id}:key/my-unique-kms-key-id",
221+
}
222+
app = core.App()
223+
PipelineStack(app, stack_input)
224+
225+
cloud_assembly = app.synth()
226+
resources = {k[0:-8]: v for k, v in cloud_assembly.stacks[0].template['Resources'].items()}
227+
pipeline_notification = resources['pipelinenoti']['Properties']
228+
229+
target = pipeline_notification["Targets"][0]
230+
231+
assert resources["pipelinenoti"]["Type"] == "AWS::CodeStarNotifications::NotificationRule"
232+
assert target["TargetAddress"] == "arn:aws:chatbot::111111111111:chat-configuration/slack-channel/fake-config"
233+
assert target["TargetType"] == "AWSChatbotSlack"
234+
assert pipeline_notification["EventTypeIds"] == [
235+
"codepipeline-pipeline-stage-execution-succeeded",
236+
"codepipeline-pipeline-stage-execution-failed",
237+
"codepipeline-pipeline-pipeline-execution-started",
238+
"codepipeline-pipeline-pipeline-execution-failed",
239+
"codepipeline-pipeline-pipeline-execution-succeeded",
240+
"codepipeline-pipeline-manual-approval-needed",
241+
"codepipeline-pipeline-manual-approval-succeeded"
242+
]
243+
assert pipeline_notification["DetailType"] == "FULL"

0 commit comments

Comments
 (0)