Cloudformation - how to set filter policy of SNS subscription in code? - aws-cloudformation

UPDATE: Cloudformation now supports SNS Topic Filters, so this question is not relevant anymore, no custom plugins or code is needed.
I am building a system with a number of SNS topics, and a number of Lambdas which are each reading messages from their assigned SQS queue. The SQS queues are subscribed to the SNS topics, but also have a filter policy so the messages will end up in the relevant SQS queues.
It works well when I set up the subscriptions in the AWS console.
Now I'm trying to do the same in my code, but the AWS Cloudformation documentation does not describe how to add a filter policy to a subscription. Based on the python examples here, I tried the following:
StopOperationSubscription:
Type: "AWS::SNS::Subscription"
Properties:
Protocol: sqs
TopicArn:
Ref: StatusTopic
Endpoint:
Fn::GetAtt: [StopActionQueue, Arn]
FilterPolicy: '{"value": ["stop"]}'
But then I get this error:
An error occurred: StopOperationSubscription - Encountered unsupported property FilterPolicy.
How can I set the filter policy that I need, using CloudFormation? And If that's not supported, what do you suggest as an alternative?
I want it to be set up automatically when I deploy my serverless app, with no manual steps required.

Cloudformation just started to support FilterPolicy yesterday. I have been struggling for a while too :)
Syntax
JSON
{
"Type" : "AWS::SNS::Subscription",
"Properties" : {
"DeliveryPolicy" : JSON object,
"Endpoint" : String,
"FilterPolicy" : JSON object,
"Protocol" : String,
"RawMessageDelivery" : Boolean,
"Region" : String,
"TopicArn" : String
}
}
YAML
Type: "AWS::SNS::Subscription"
Properties:
DeliveryPolicy: JSON object
Endpoint: String
FilterPolicy: JSON object
Protocol: String
RawMessageDelivery: Boolean,
Region: String
TopicArn: String
Ref:
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-subscription.html#cfn-sns-subscription-filterpolicy
https://aws.amazon.com/blogs/compute/managing-amazon-sns-subscription-attributes-with-aws-cloudformation/

I fixed it like this:
serverless.yml
plugins:
- serverless-plugin-scripts
custom:
scripts:
commands:
update-topic-filters: sls invoke local -f configureSubscriptions --path resources/lambdaTopicFilters.json
hooks:
before:deploy:finalize: sls update-topic-filters
functions:
configureSubscriptions:
handler: src/configurationLambdas/configureSubscriptions.main
# Only invoked when deploying - therefore, no permissions or triggers are needed.
configureSubscriptions.js
import AWS from 'aws-sdk'
const nameFromArn = arn => arn.split(':').pop()
const lambdaNameFromArn = arn =>
nameFromArn(arn)
.split('-')
.pop()
exports.main = async event => {
const sns = new AWS.SNS({ apiVersion: '2010-03-31' })
const params = {}
const { Topics } = await sns.listTopics(params).promise()
for (const { TopicArn } of Topics) {
const topicName = nameFromArn(TopicArn)
const filtersForTopic = event[topicName]
if (!filtersForTopic) {
continue
}
const { Subscriptions } = await sns.listSubscriptionsByTopic({ TopicArn }).promise()
for (const { Protocol, Endpoint, SubscriptionArn } of Subscriptions) {
if (Protocol === 'lambda') {
const lambdaName = lambdaNameFromArn(Endpoint)
const filterForLambda = filtersForTopic[lambdaName]
if (!filterForLambda) {
continue
}
const setPolicyParams = {
AttributeName: 'FilterPolicy',
SubscriptionArn,
AttributeValue: JSON.stringify(filterForLambda),
}
await sns.setSubscriptionAttributes(setPolicyParams).promise()
// eslint-disable-next-line no-console
console.log('Subscription filters has been set')
}
}
}
}
Top level is the different topic names, next level is the lambda names, and the third level is the filter policies for the related subscriptions:
lambdaTopicFilters.json
{
"user-event": {
"activateUser": {
"valueType": ["status"],
"value": ["awaiting_activation"]
},
"findActivities": {
"messageType": ["event"],
"value": ["awaiting_activity_data"],
"valueType": ["status"]
}
},
"system-event": {
"startStopProcess": {
"valueType": ["status"],
"value": ["activated", "aborted", "limit_reached"]
}
}
}

If you are using serverless it is now supporting sns filter natively
functions:
pets:
handler: pets.handler
events:
- sns:
topicName: pets
filterPolicy:
pet:
- dog
- cat
https://serverless.com/framework/docs/providers/aws/events/sns#setting-a-filter-policy

Related

Referencing REST API resource from CDK in amplify custom resource

I need to customize the method handler for a REST API endpoint and point it to SQS instead of a Lambda function. I'm stalling out trying to get a full reference to the RestApi object...
The RestApi object I get back from RestApi.fromRestApiId is incomplete; I can't do this:
const restApi = apigateway.RestApi.fromRestApiId(this, 'RestApi', dependencies.api.rest.ApiId);
const queueResource = restApi.root.resourceForPath('/webhooks');
...without getting this error:
Error: root is not configured when imported using fromRestApiId(). Use fromRestApiAttributes() API instead.
I can't use RestApi.fromRestApiAttributes as that requires the rootResourceId -- which I can't seem to find a reference to. The documentation for RestApi.fromRestApiAttributes shows this, but I don't have props:
const api = RestApi.fromRestApiAttributes(this, 'RestApi', {
restApiId: props.restApiId,
rootResourceId: props.rootResourceId,
});
Does anyone know how to access the rootResourceId?
The root resource (/) id is a alphanumeric string like 4cfzeywftb, which can be found in the console breadcrumbs:
APIs > API (076t2zozc0) > Resources> / (4cfzeywftb)
or by calling get-resources:
aws apigateway get-resources --rest-api-id 076t2zozc0
{
"items": [
{
"id": "4cfzeywftb",
"path": "/",
"resourceMethods": {
"ANY": {}
}
},
{
"id": "36g7tq",
"parentId": "4cfzeywftb",
"pathPart": "{proxy+}",
"path": "/{proxy+}",
"resourceMethods": {
"ANY": {}
}
}
]
}

Unable to deploy smart contract to testnet (Ropsten) using Truffle Infura

I'm trying to deploy a simple smart contract to Testnet Ropsten but always fails with the following error:
Error: *** Deployment Failed ***
"Migrations" -- Transaction was not mined within 750 seconds, please
make sure your transaction was properly sent. Be aware that it might
still be mined!.
I've searched in many places, and it says to change the gas value, I tried almost every value but still got the same error. Sometimes it says that the gas value is not enough and sometimes it's too much.
This is my code:
require("dotenv").config();
const HDWalletProvider = require("truffle-hdwallet-provider");
const MNEMONIC = process.env.MNEMONIC;
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*",
},
ropsten: {
provider: function () {
return new HDWalletProvider(
MNEMONIC,
`https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`
);
},
network_id: 3,
gas: 5500000, // Here I tried from 1000000 to 5500000
},
},
compilers: {
solc: {
version: "0.8.10",
},
},
};
Contract:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.3;
contract HelloWorld {
function sayHello() public pure returns(string memory){
return("hello world");
}
}
Migrations:
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};
const HelloWorld = artifacts.require("HelloWorld");
module.exports = function (deployer) {
deployer.deploy(HelloWorld, "hello");
};
I have my metamask wallet with 1 ether in red ropstem
Any idea on how to deploy a smart contract in a testnet?
It might be because, you are not speciying address and gasPrice. gasPrice varies depending on network activities. You can get the current gasPrice here : https://ethgas.watch/
ropsten: {
provider: () =>
new HDWalletProvider({
mnemonic: {
phrase: MENMONICS,
},
providerOrUrl: `https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`,
// first address
addressIndex: 0,
}),
network_id: 3,
gas: 5500000,
// this might varies depending on the network activity
gasPrice: 20000000000,
confirmations: 2, // number of blocks to wait between deployment
timeoutBlocks: 200, // number of blocks before deployment times out
},

CDK API Gateway default authorizer exclude OPTIONS method

I'm creating a LambdaRestApi in CDK and I want to have both CORS enabled and add an ANY proxy using the addProxy method.
I currently have the following CDK code:
const api = new LambdaRestApi(...); // This API has CORS enabled in defaultCorsPreflightOptions
const apiProxy = api.root.addProxy({
defaultMethodOptions: {
authorizationType: AuthorizationType.COGNITO,
authorizer: new CognitoUserPoolsAuthorizer(...),
}
});
The problem I'm running into is that while a proxy is created with the ANY method, it also sets the OPTIONS method to require authentication. I tried to add an OPTIONS method to the proxy using addMethod to override the authorizer but I get an error that there's already a construct with the same name. I'm also trying to avoid having to set the anyMethod field in the proxy to be false and adding my own methods. Is there a way in the API Gateway CDK to set the default authorizer to only work for any method except the OPTIONS method?
There is also a possibility to remove the auth explicitly on the OPTIONS method, here I also remove requirement for X-API-Key in the requests
const lambdaApi = new apigateway.LambdaRestApi(...) // This API has CORS enabled in defaultCorsPreflightOptions
lambdaApi.methods
.filter((method) => method.httpMethod === "OPTIONS")
.forEach((method) => {
const methodCfn = method.node.defaultChild as apigateway.CfnMethod;
methodCfn.authorizationType = apigateway.AuthorizationType.NONE;
methodCfn.authorizerId = undefined;
methodCfn.authorizationScopes = undefined;
methodCfn.apiKeyRequired = false;
});
I ran into the same issue when building a RestApi using the aws cdk. Here is a workaround where you can build the api piece by piece.
Declare the api construct without the defaultCorsPreflightOptions property, otherwise you will not be able to override Authorization on the OPTIONS method.
import * as apigateway from '#aws-cdk/aws-apigateway';
import * as lambda from '#aws-cdk/aws-lambda';
const restAPI = new apigateway.RestApi(this, "sample-api");
Add your resources and methods. In this case, I want to add an ANY method with a custom authorizer on the "data" resource. You can do this with any other supported authorization mechanism. The proxy handler is a lambda function created with NodeJs.
const dataProxyLambdaFunction = new lambda.Function(this, "data-handler", {
code: lambda.Code.fromBucket(S3CODEBUCKET, "latest/js_data.zip"),
handler: "index.handler",
runtime: lambda.Runtime.NODEJS_14_X
});
const dataProxy = restAPI.root.addResource("data")
.addResource("{proxy+}");
dataProxy.addMethod("ANY", new apigateway.LambdaIntegration(dataProxyLambdaFunction , {
allowTestInvoke: true,
}), { authorizationType: apigateway.AuthorizationType.CUSTOM, authorizer: customLambdaRequestAuthorizer });
Now, you can add an OPTIONS method to this proxy, without authorization. I used a standardCorsMockIntegration and optionsMethodResponse object to reuse with other methods in my api.
const ALLOWED_HEADERS = ['Content-Type', 'X-Amz-Date', 'X-Amz-Security-Token', 'Authorization', 'X-Api-Key', 'X-Requested-With', 'Accept', 'Access-Control-Allow-Methods', 'Access-Control-Allow-Origin', 'Access-Control-Allow-Headers'];
const standardCorsMockIntegration = new apigateway.MockIntegration({
integrationResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': `'${ALLOWED_HEADERS.join(",")}'`,
'method.response.header.Access-Control-Allow-Origin': "'*'",
'method.response.header.Access-Control-Allow-Credentials': "'false'",
'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
},
}],
passthroughBehavior: apigateway.PassthroughBehavior.NEVER,
requestTemplates: {
"application/json": "{\"statusCode\": 200}"
}
});
const optionsMethodResponse = {
statusCode: '200',
responseModels: {
'application/json': apigateway.Model.EMPTY_MODEL
},
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': true,
'method.response.header.Access-Control-Allow-Methods': true,
'method.response.header.Access-Control-Allow-Credentials': true,
'method.response.header.Access-Control-Allow-Origin': true,
}
};
dataProxy.addMethod("OPTIONS", standardCorsMockIntegration, {
authorizationType: apigateway.AuthorizationType.NONE,
methodResponses: [
optionsMethodResponse
]
});
When you deploy the API, you can verify using the API Gateway console that your methods have been setup correctly. The proxy's ANY method has authorization enabled while the OPTIONS method does not.
Reference to the GitHub issue that helped: GitHub Issue 'apigateway: add explicit support for CORS'

How to target Route53 A record to ApiGateway v2

There's an alias for ApiGateway 1, but it's interface doesn't conform to V2:
Here's domainName:
const domainName = new apigw2.DomainName(config.scope, config.id + 'DomainName', {
domainName: config.domainName,
certificate: config.certificate,
});
It doesn't look like aws-route53-targets packages supports apigatewayv2 yet. In the meantime you can probably wrap the v2 object in the v1 interface like this:
new route53.ARecord(config.scope, config.id + "AliasRecord", {
recordName: config.domainName,
target: route53.RecordTarget.fromAlias(
new route53targets.ApiGatewayDomain({
...domainName,
domainNameAliasDomainName: domainName.regionalDomainName,
domainNameAliasHostedZoneId: domainName.regionalHostedZoneId
})
),
zone: config.hostedZone
});
The documentation for Custom Domain is the starting point. However, it does not generate any record in Route53 by default.
To get this sorted for ApiGatewayV2:
Follow the steps as in the mentioned documentation and set up your API Gateway.
You need to use #aws-cdk/aws-route53 and #aws-cdk/aws-route53-targets and the example that mentions API Gateway V2.
So that:
// From the API Gateway setup (step 1)
const apiProdDomain = new DomainName(this, '...', {...})
...
new r53.ARecord(this, 'YourDomainAliasRecord', {
zone: yourDomainHostedZone,
recordName: yourDomainPrefix, // i.e 'api' for api.xxx.com
target: r53.RecordTarget.fromAlias(new ApiGatewayv2DomainProperties(apiProdDomain.regionalDomainName, apiProdDomain.regionalHostedZoneId)
})
That's it.
You could try it the way it's described in the documentation for Custom Domain
const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate';
const domainName = 'example.com';
const dn = new DomainName(stack, 'DN', {
domainName,
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', certArn),
});
const api = new HttpApi(stack, 'HttpProxyProdApi', {
defaultIntegration: new LambdaProxyIntegration({ handler }),
// https://${dn.domainName}/foo goes to prodApi $default stage
defaultDomainMapping: {
domainName: dn,
mappingKey: 'foo',
},
});

how to sync data from ydn-db web app to backend server?

With ydn-dn, i want to automatically synchronise data from my web app with my REST back end.
I read the documentation and searched in examples but i cannot make it work.
https://yathit.github.io/ydn-db/synchronization.html
http://dev.yathit.com/api/ydn/db/schema.html#sync
I tried to define a schema with sync configuration like that :
var schema = {
stores: [ {
name: 'contact',
keyPath: 'id',
Sync: {
format: 'rest',
transport: service,
Options: {
baseUri: '/'
}
}
}
]
};
and created a function for transport :
var service = function(args) {
console.log("contact synch");
};
but my service function is never called.
I certainly misunderstood how YDN-db work, but i didn't found any example.
To complete, here is a jsfiddle :
http://jsfiddle.net/asicfr/y7sL7b3j/
Please see the example http://yathit.github.io/ydndb-demo/entity-sync/app.html
Older example http://yathit.github.io/sprintly-service/playground.html from https://github.com/yathit/sprintly-service