Is it possible to reuse pre-built Text in CloudFormation? - aws-cloudformation

I have a few parameters passed to one of my CloudFormation conf file like this (YAML):
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS CloudFormation Template for creating pipeline workflow.
Parameters:
Opt1:
Description: my opt1
Type: String
Default: opt1_default
Opt2:
Description: my opt2
Type: String
Default: opt2_default
Opt3:
Description: my opt3
Type: String
Default: opt3_default
...
In many other parts of my conf I keep on use:
...
Name: !Sub '${Opt1}-${Opt2}-${Opt3}'
...
Name: !Sub '${Opt1}-${Opt2}-${Opt3}'
...
Name: !Sub '${Opt1}-${Opt2}-${Opt3}'
...
Is it possible to create some reference like:
SomeRef: !Sub '${Opt1}-${Opt2}-${Opt3}'
in a way that I can do:
...
Name: !Ref SomeRef
...
Name: !Ref SomeRef
...
Name: !Ref SomeRef
...
?

Nested template with no resources and single output can do it:
NameCreator.yaml
---
AWSTemplateFormatVersion: 2010-09-09
Description: 'Empty nested template'
Parameters:
Opt1:
Type: String
Opt2:
Type: String
Conditions:
Never: !Equals [ a, b ]
Resources:
NullResource:
Type: Custom::Null
Condition: Never
Outputs:
Name:
Value: !Sub '${Opt1}-${Opt2}'
And then in main template call it:
NameCreator:
Type: AWS::CloudFormation::Stack
Properties:
Parameters:
Opt1: !Ref Opt1
Opt2: !Ref Opt2
TemplateURL: https://mybucket.s3.amazonaws.com/NameCreator.yaml
and use it:
Name: !GetAtt 'NameCreator.Outputs.Name'

Related

How to conditionally create an AWS::Events::Connection resource based on if a secret key is defined or not

I currently have this cloud formation template that is supposed to create an event bridge resource with all the necessary configurations, but I can't get it to create because I can't get cloud formation to verify if a key exists in the secrets manager or not.
to be more clear, I want my event-bridge.yml template resource to only be created if my key ${Stage}/${SubDomain}/django-events-api-key is already defined in the secrets manager and has a valid value(meaning it does not only have an empty string or a AWS::NoValue); this because I need to create the key after the stack is created and deployed, before the stack is not deployed, so I can't execute my command to generate the key
I have this:
event-bridge.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Event scheduling for sending the email digest"
Parameters:
SubDomain:
Description: The part of a website address before your DomainName - e.g. www or img
Type: String
DomainName:
Description: The part of a website address after your SubDomain - e.g. example.com
Type: String
Stage:
Description: Stage name (e.g. dev, test, prod)
Type: String
DjangoApiKey:
Description: Api key for events bridge communication
Type: String
Conditions:
DjangoApiKeyExists: !Or [ !Not [ !Equals [ !Ref DjangoApiKey, !Ref AWS::NoValue ] ], !Not [ !Equals [ !Ref DjangoApiKey, "" ] ] ]
Outputs:
DjangoEventsConnection:
Description: Connection to the Django backend for Event Bridge
Value: !Ref DjangoEventsConnection
Resources:
MessageDigestEventsRule:
Type: AWS::Events::Rule
Properties:
Name: !Sub "${SubDomain}-chat-digest"
Description: "Send out email digests for a chat"
ScheduleExpression: "rate(15 minutes)"
State: "ENABLED"
Targets:
- Arn: !GetAtt MessageDigestEventsApiDestination.Arn
HttpParameters:
HeaderParameters: { }
QueryStringParameters: { }
Id: !Sub "${SubDomain}-chat-digest-api-target"
RoleArn: !GetAtt MessageDigestEventsRole.Arn
EventBusName: "default"
DjangoEventsConnection:
Type: AWS::Events::Connection
Properties:
Name: !Sub "${SubDomain}-django"
AuthorizationType: "API_KEY"
AuthParameters:
ApiKeyAuthParameters:
ApiKeyName: "Authorization"
ApiKeyValue: !Ref DjangoApiKey
in a main.yml template I pass the key variable like this:
EventBridge:
DependsOn: [ VpcStack, DjangoEventBridgeApiKey ]
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./event-bridge.yaml
Parameters:
SubDomain: !Ref SubDomain
DomainName: !Ref DomainName
Stage: !Ref Stage
DjangoApiKey: !Sub '{{resolve:secretsmanager:arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SubDomain}/django-events-api-key}}' <--
but that will always fail because the key is not defined, I would like to pass an empty string or something I can use as a conditional
I have also tried defining the secret, so it exists:
Resources:
DjangoEventBridgeApiKey:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub ${Stage}/${SubDomain}/django-events-api-key
Description: !Sub Credentials for the event bridge integration https://api.${SubDomain}.circular.co
Tags:
- Key: Name
Value: django-events-api-key
EventBridge:
DependsOn: [ VpcStack, DjangoEventBridgeApiKey ]
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./event-bridge.yaml
Parameters:
SubDomain: !Ref SubDomain
DomainName: !Ref DomainName
Stage: !Ref Stage
DjangoApiKey: !Sub '{{resolve:secretsmanager:${DjangoEventBridgeApiKey}}}'
But that for some reason, is still making my condition above fail, making the stack attempt to execute, I still cant figure out why is my condition not working
Any ideas on how to make this better? any help provided will be super useful to me
Okay found out the biggest problem with my implementation, on event-bridge.yml:
AWSTemplateFormatVersion: "2010-09-09"
Description: "Event scheduling for sending the email digest of chat messages"
Parameters:
SubDomain:
Description: The part of a website address before your DomainName - e.g. www or img
Type: String
DomainName:
Description: The part of a website address after your SubDomain - e.g. example.com
Type: String
Stage:
Description: Stage name (e.g. dev, test, prod)
Type: String
DjangoApiKey:
Description: Api key for events bridge communication
Type: String
Conditions:
DjangoApiKeyExists: !Not [ !Equals [ !Ref DjangoApiKey, "" ] ] # <-- this works
Outputs:
DjangoEventsConnection:
Condition: DjangoApiKeyExists
Description: Connection to the django backend for Event Bridge
Value: !Ref DjangoEventsConnection
Resources:
DjangoEventsConnection:
Type: AWS::Events::Connection
Condition: DjangoApiKeyExists
Properties:
Name: !Sub "${SubDomain}-django"
AuthorizationType: "API_KEY"
AuthParameters:
ApiKeyAuthParameters:
ApiKeyName: "Authorization"
ApiKeyValue: !Ref DjangoApiKey
# This does not update when we change the secret - so we need to force an update - need a more permanent solution
# ApiKeyValue: pop
MessageDigestEventsApiDestination:
Type: AWS::Events::ApiDestination
Condition: DjangoApiKeyExists
DependsOn: DjangoEventsConnection
on main.yml
...
DjangoEventBridgeApiKey:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub ${Stage}/${SubDomain}/django-events-api-key # <- this was missing ${Stage}
SecretString: " "
Tags:
- Key: Name
Value: django-events-api-key
EventBridge:
DependsOn: DjangoEventBridgeApiKey
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./event-bridge.yaml
Parameters:
SubDomain: !Ref SubDomain
DomainName: !Ref DomainName
Stage: !Ref Stage
DjangoApiKey: !Sub '{{resolve:secretsmanager:arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Stage}/${SubDomain}/django-events-api-key}}'
Tags:
- Key: Stage
Value: !Ref Stage
- Key: SubDomain
Value: !Ref SubDomain
- Key: SecretKeyName
Value: !Ref DjangoEventBridgeApiKey

AWS API Gateway with Lambda Authorizer

I am trying to configure an API Gateway which takes a proxy parameter from the request path, and also a parameter from the Lambda authorizer return and put it in the header, so that it can be passed to my Elastic Beanstalk REST API running Spring Boot.
The proxy path is working as expected; and I see my Lambda function is returning the variable "x-api-auth" in the "context" map as per documentation.
The only piece not working is adding "x-api-auth" to the request header. :( Whenever I ran my Jenkins build to update the Cloudformation stack, I get this error:
Errors found during import: Unable to put integration on 'ANY' for resource at path '/sfdc/v1/feature-api/{proxy+}': Invalid mapping expression specified: Validation Result: warnings : [], errors : [Invalid mapping expression specified: $context.authorizer.x-api-auth] (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException
It is super frustrating and I've double checked OpenAPI documentation to make sure my syntax is correct. Any help or tips would be most appreciated!
Here is the Cloudformation template I have:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Data API pipeline initial Cloudformation template
Mappings:
EnvironmentMapping:
alpha:
certificationArn: ""
carfaxIpWhitelistRuleId: ""
hostedZoneId: XYZ
authLambda: ""
sfdcAuthLambda: ""
myApiNetworkLoadBalancer: ""
sfdcAuthTimeout: 1
beta:
certificationArn: ""
carfaxIpWhitelistRuleId: ""
hostedZoneId: XYZ
authLambda: ""
sfdcAuthLambda: ""
myApiNetworkLoadBalancer: ""
sfdcAuthTimeout: 1
prod:
certificationArn: ""
carfaxIpWhitelistRuleId: ""
hostedZoneId: ABC
authLambda: ""
sfdcAuthLambda: ""
myApiNetworkLoadBalancer: ""
sfdcAuthTimeout: 1
Parameters:
EnvironmentType:
Type: "String"
AllowedValues:
- alpha
- beta
- prod
Conditions:
UseProdCondition: !Equals [!Ref EnvironmentType, prod]
Resources:
MyApiVpcLink:
Type: AWS::ApiGateway::VpcLink
Properties:
Name: MyApiVpcLink
Description: Allows data-api-gateway to access the VPC that my-api is on.
TargetArns:
- !FindInMap [EnvironmentMapping, !Ref EnvironmentType, myApiNetworkLoadBalancer]
DataApi:
DependsOn:
- MyApiVpcLink
Type: AWS::Serverless::Api
Properties:
Name: !Sub "${EnvironmentType}-data-api"
StageName: !Ref EnvironmentType
DefinitionBody:
swagger: 2.0
security:
- ApiKey: []
info:
title: !Sub "${EnvironmentType}-data-api"
paths:
/sfdc/v1/my-api/{proxy+}:
x-amazon-apigateway-any-method:
produces:
- application/json
parameters:
- in: path
name: proxy
required: true
schema:
type: string
- in: header
name: x-api-auth
required: true
schema:
type: string
security:
- SfdcAuthorizer: []
ApiKey: []
x-amazon-apigateway-api-key-source: HEADER
x-amazon-apigateway-gateway-responses:
ACCESS_DENIED:
statusCode: 403
responseTemplates:
application/json: '{\n\"message\": \"Access Denied\"}'
x-amazon-apigateway-integration:
httpMethod: ANY
type: http_proxy
connectionType: VPC_LINK
connectionId: !Ref MyApiVpcLink
passthroughBehavior: when_no_match
uri: !If [UseProdCondition, 'http://myapp.production.aws-int.myorg.io/{proxy}',!Sub 'http://${EnvironmentType}-myapp.staging.aws-int.myorg.io/{proxy}']
requestParameters:
integration.request.path.proxy: "method.request.path.proxy"
# -------------------- this breaks it once added -------------------
integration.request.header.x-api-auth: "$context.authorizer.x-api-auth"
# ------------------------------------------------------------------
definitions:
Empty:
type: object
Error:
type: object
properties:
message:
type: string
securityDefinitions:
SfdcAuthorizer:
type: 'apiKey'
name: 'Authorization'
in: 'header'
x-amazon-apigateway-authtype: 'custom'
x-amazon-apigateway-authorizer:
authorizerUri: !Join ['', [!Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/', !FindInMap [EnvironmentMapping, !Ref EnvironmentType, sfdcAuthLambda], '/invocations']]
authorizerResultTtlInSeconds: !FindInMap [EnvironmentMapping, !Ref EnvironmentType, sfdcAuthTimeout]
type: 'token'
ApiKey:
type: apiKey
name: x-api-key
in: header
Well... after getting nowhere with following the documentation, I went rogue and removed the "$" from "integration.request.header.x-api-auth"... AND THAT WORKED. Not sure how I feel about this.
Here is the complete working YAML file. I'm posting it here in case it should help someone else who is trying to set up a gateway which takes PROXY path and expects a return from a Lambda authorizer.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Data API pipeline initial Cloudformation template
Mappings:
EnvironmentMapping:
alpha:
certificationArn: ""
carfaxIpWhitelistRuleId: ""
hostedZoneId: XYZ
authLambda: ""
sfdcAuthLambda: ""
myApiNetworkLoadBalancer: ""
sfdcAuthTimeout: 1
beta:
certificationArn: ""
carfaxIpWhitelistRuleId: ""
hostedZoneId: XYZ
authLambda: ""
sfdcAuthLambda: ""
myApiNetworkLoadBalancer: ""
sfdcAuthTimeout: 1
prod:
certificationArn: ""
carfaxIpWhitelistRuleId: ""
hostedZoneId: ABC
authLambda: ""
sfdcAuthLambda: ""
myApiNetworkLoadBalancer: ""
sfdcAuthTimeout: 1
Parameters:
EnvironmentType:
Type: "String"
AllowedValues:
- alpha
- beta
- prod
Conditions:
UseProdCondition: !Equals [!Ref EnvironmentType, prod]
Resources:
MyApiVpcLink:
Type: AWS::ApiGateway::VpcLink
Properties:
Name: MYApiVpcLink
Description: Allows data-api-gateway to access the VPC that feature-api is on.
TargetArns:
- !FindInMap [EnvironmentMapping, !Ref EnvironmentType, myApiNetworkLoadBalancer]
DataApi:
DependsOn:
- MyApiVpcLink
Type: AWS::Serverless::Api
Properties:
Name: !Sub "${EnvironmentType}-data-api"
StageName: !Ref EnvironmentType
DefinitionBody:
swagger: 2.0
security:
- ApiKey: []
info:
title: !Sub "${EnvironmentType}-data-api"
paths:
/sfdc/v1/my-api/{proxy+}:
x-amazon-apigateway-any-method:
produces:
- application/json
parameters:
- in: path
name: proxy
required: true
schema:
type: string
- in: header
name: x-api-auth
required: true
schema:
type: string
security:
- SfdcAuthorizer: []
ApiKey: []
x-amazon-apigateway-api-key-source: HEADER
x-amazon-apigateway-gateway-responses:
ACCESS_DENIED:
statusCode: 403
responseTemplates:
application/json: '{\n\"message\": \"Access Denied\"}'
x-amazon-apigateway-integration:
httpMethod: ANY
type: http_proxy
connectionType: VPC_LINK
connectionId: !Ref MyApiVpcLink
passthroughBehavior: when_no_match
uri: !If [UseProdCondition, 'http://myapp.production.aws-int.myorg.io/{proxy}',!Sub 'http://${EnvironmentType}-myapp.staging.aws-int.myorg.io/{proxy}']
requestParameters:
integration.request.path.proxy: "method.request.path.proxy"
integration.request.header.x-api-auth: "context.authorizer.x-api-auth"
definitions:
Empty:
type: object
Error:
type: object
properties:
message:
type: string
securityDefinitions:
SfdcAuthorizer:
type: 'apiKey'
name: 'Authorization'
in: 'header'
x-amazon-apigateway-authtype: 'custom'
x-amazon-apigateway-authorizer:
authorizerUri: !Join ['', [!Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/', !FindInMap [EnvironmentMapping, !Ref EnvironmentType, sfdcAuthLambda], '/invocations']]
authorizerResultTtlInSeconds: !FindInMap [EnvironmentMapping, !Ref EnvironmentType, sfdcAuthTimeout]
type: 'token'
ApiKey:
type: apiKey
name: x-api-key
in: header

deploy multiple services using serverless to Apigateway with shared path

The documentation addresses shared paths:
service: service-b
provider:
apiGateway:
restApiId: xxxxxxxxxx
restApiRootResourceId: xxxxxxxxxx
restApiResources:
/reports: xxxxxxxxxx
functions:
...
However, how can I reference the ID of the resource (the path, that is)? In the first service I have:
Outputs:
apiGatewayRestApiId:
Value:
Ref: ApiGatewayRestApi
Export:
Name: ${self:service}-${opt:stage, 'dev'}-apiGateway-restApiId
apiGatewayRestApiRootResourceId:
Value:
Fn::GetAtt:
- ApiGatewayRestApi
- RootResourceId
Export:
Name: ${self:service}-${opt:stage, 'dev'}-apiGateway-rootResourceId
apiGatewayResourceReports:
Value: !Ref ApiGatewayResource/reports
Export:
Name: ${self:service}-${opt:stage, 'dev'}-apiGateway-reportPath
The first two work, and can be referred to with FN::ImportValue in the 2nd service. However, the third doesn't work. I presume the problem is
that I have to explicitly create the resource ApiGatewayResource/reports
rather than have it created as a side effect of the function definitions in the first service. But how should I do that? And won't it conflict with the function definitions?
After some floundering, I hit upon the following: the first service should
define the resource path, but leave the rest of the gateway definition implicit. And it should output the relevant ids:
provider:
apiGateway:
restApiResources:
/reports: !Ref ReportPath
...
resources:
Resources:
ReportPath:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref ApiGatewayRestApi
ParentId:
Fn::GetAtt: [ ApiGatewayRestApi, RootResourceId ]
PathPart: 'reports'
Outputs:
apiGatewayRestApiId:
Value:
Ref: ApiGatewayRestApi
Export:
Name: ${self:service}-${opt:stage, 'dev'}-apiGateway-restApiId
apiGatewayRestApiRootResourceId:
Value:
Fn::GetAtt:
- ApiGatewayRestApi
- RootResourceId
Export:
Name: ${self:service}-${opt:stage, 'dev'}-apiGateway-rootResourceId
apiGatewayResourceReports:
Value: !Ref ReportPath
Export:
Name: ${self:service}-${opt:stage, 'dev'}-apiGateway-reportPath
The 2nd service can reference all three ids:
provider:
apiGateway:
restApiId:
'Fn::ImportValue': crane-mg-reports-${opt:stage, 'dev'}-apiGateway-restApiId
restApiRootResourceId:
'Fn::ImportValue': crane-mg-reports-${opt:stage, 'dev'}-apiGateway-rootResourceId
restApiResources:
/reports:
'Fn::ImportValue': crane-mg-reports-${opt:stage, 'dev'}-apiGateway-reportPath
In both cases, the function definition can define paths using /reports prefix without conflict.

Reference multiple Parameter Cloudformation AWS

If I have some parameter like
Parameters:
Owner:
Description: Enter Team or Individual Name Responsible for the Stack.
Type: String
Default: Name
Project:
Description: Enter Project Name.
Type: String
Default: Whatever
is there a way to reference them both like:
Resources:
Resource:
Properties:
Name: !Ref Owner- !Sub ${Project}
merci A
You can do
Name: !Sub "${Owner}-${Project}"
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html#w2ab1c21c28c59b7
You can use:
Name: !Join [ '-', [!Ref Owner, !Ref Project] ]
Which will generate something like ownerX-projectY

Nested Fn::ImportValue in Fn::Sub not working for SAM template

Description:
I am trying to define Serverless API resource. But having trouble in defining location of swagger specification file using function ImportValue.
Steps to reproduce the issue:
I am not able to define AWS::Serverless::Api resource having nested function ImportValue in Location. I have tried following three ways, none of them work.
Note: Stack parameters are defined properly and export value from other stack exists. Not showing them here for brevity reason.
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
Name: !Sub ${AWS::StackName}-API
StageName: !Ref ApiGatewayStageName
DefinitionBody:
'Fn::Transform':
Name: 'AWS::Include'
Parameters:
Location:
Fn::Sub:
- s3://${BucketName}/${SwaggerSpecificationS3Key}
- BucketName:
Fn::ImportValue:
!Sub "${EnvironmentName}-dist-bucket-${AWS::Region}"
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
Name: !Sub ${AWS::StackName}-API
StageName: !Ref ApiGatewayStageName
DefinitionBody:
'Fn::Transform':
Name: 'AWS::Include'
Parameters:
Location:
Fn::Sub:
- s3://${BucketName}/${SwaggerSpecificationS3Key}
- BucketName:
!ImportValue 'dev-dist-bucket-us-east-1'
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
Name: !Sub ${AWS::StackName}-API
StageName: !Ref ApiGatewayStageName
DefinitionBody:
'Fn::Transform':
Name: 'AWS::Include'
Parameters:
Location:
Fn::Sub:
- s3://${BucketName}/${SwaggerSpecificationS3Key}
- BucketName:
Fn::ImportValue: 'dev-dist-bucket-us-east-1'
Cloudformation shows following error.
FAILED - The value of parameter Location under transform Include must
resolve to a string, number, boolean or a list of any of these.
However, if I do not use ImportValue it works with a nested Fn::Sub
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
Name: !Sub ${AWS::StackName}-API
StageName: !Ref ApiGatewayStageName
DefinitionBody:
'Fn::Transform':
Name: 'AWS::Include'
Parameters:
Location:
Fn::Sub:
- s3://${BucketName}/${SwaggerSpecificationS3Key}
- BucketName:
Fn::Sub: dist-bucket-${EnvironmentName}-${AWS::Region}
Is it because of Fn::Transform or AWS::Include?