View on GitHub

cfn-generic-custom-resource

CloudFormation generic custom resource provider

cfn-custom-resource-provider Build Status

TL;DR One Custom Resource provider to Rule Them All, inspect the code, read the blog, try some examples and consider contributing ๐Ÿค“

TOC

CloudFormation demo stacks

mock requests

client

resources

about

The idea behind this project was to make available a flexible and simple tool to enable creation of any AWS resource supported by the API. We implement this functionality via a generic CloudFormation Custom Resources provider in Python (3), using boto3. The word โ€œgenericโ€ is used here in a sense of having just one Lambda function, which can be used to create different custom resources by varying the input parameters.

For more information, please read this blog post.

CloudFormation

All shell-fu is Bash; git, pip, awscli and jq required.

init

git clone https://github.com/ab77/cfn-generic-custom-resource\
  && cd cfn-generic-custom-resource

create bucket

๐Ÿ“ creates a new bucket with a random GUID; ensure ~/.aws/credentials and ~/.aws/config are configured (run aws configure ...) and export AWS_PROFILE and AWS_REGION environment variables

bucket=$(uuid)
aws s3 mb s3://${bucket}

install requirements (venv)

๐Ÿ“ AWS Lambda provided boto3 library doesnโ€™t support Client VPN resources at the time of writing, so we need to package it with the code

sudo pip install venv --user || sudo pip install virtualenv --user\
  && pushd generic_provider\
  && python -m venv venv || python -m virtualenv venv\
  && . venv/bin/activate\
  && pip install --upgrade pip\
  && pip install --upgrade -r requirements.txt -t .\
  && popd

Client VPN demo

โ˜ข๏ธ beware of the currently eye-watering Client VPN pricing

certificates

๐Ÿ“œ issue certificates with easy-rsa and upload to ACM, using fictional domain foo.bar

domain_name='foo.bar'

git clone https://github.com/OpenVPN/easy-rsa

pushd easy-rsa/easyrsa3

./easyrsa init-pki

./easyrsa build-ca nopass

./easyrsa build-server-full server.${domain_name} nopass\
  && ./easyrsa build-client-full client1.${domain_name} nopass

popd

server_certificate=$(aws acm import-certificate\
  --certificate file://easy-rsa/easyrsa3/pki/issued/server.${domain_name}.crt\
  --private-key file://easy-rsa/easyrsa3/pki/private/server.${domain_name}.key\
  --certificate-chain file://easy-rsa/easyrsa3/pki/ca.crt | jq -r '.CertificateArn')

client_certificate=$(aws acm import-certificate\
  --certificate file://easy-rsa/easyrsa3/pki/issued/client1.${domain_name}.crt\
  --private-key file://easy-rsa/easyrsa3/pki/private/client1.${domain_name}.key\
  --certificate-chain file://easy-rsa/easyrsa3/pki/ca.crt | jq -r '.CertificateArn')

package assets

๐Ÿ“ฆ package CloudFormation templates and Lambda function(s) and upload to S3

pushd client-vpn; for template in lambda main client-vpn; do
    aws cloudformation package\
      --template-file ${template}-template.yaml\
      --s3-bucket ${bucket}\
      --output-template-file ${template}.yaml
done; popd

deploy stack

๐Ÿ“ creates Client VPN endpoint with certificate-authentication; for directory-service-authentication or both, specify additional DirectoryId parameter

stack_name='client-vpn-demo'
vpc_id=$(aws ec2 describe-vpcs | jq -r .Vpcs[0].VpcId)
subnets=(
    $(aws ec2 describe-subnets | jq -r ".Subnets[0] | select(.VpcId==\"${vpc_id}\").SubnetId")
    $(aws ec2 describe-subnets | jq -r ".Subnets[1] | select(.VpcId==\"${vpc_id}\").SubnetId")
)
subnet_count=${#subnets[@]}
cidr=172.16.0.0/22


pushd client-vpn; aws cloudformation deploy\
  --template-file main.yaml\
  --stack-name ${stack_name}\
  --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM\
  --parameter-overrides\
  VpcId=${vpc_id}\
  CidrBlock=${cidr}\
  SubnetIds=$(echo ${subnets[*]} | tr ' ' ',')\
  SubnetCount=${subnet_count}\
  ServerCertificateArn=${server_certificate}\
  ClientRootCertificateChainArn=${client_certificate}\
  --tags\
  Name=${stack_name}\
  Region=${AWS_REGION}\
  Profile=${AWS_PROFILE}\
  AccountId=$(aws sts get-caller-identity | jq -r '.Account'); popd

download profile

vpn_stack=$(aws cloudformation list-exports\
  | jq -r ".Exports[] | select(.Name==\"VPNStackName-${stack_name}\").Value")

client_vpn_endpoint=$(aws cloudformation list-exports\
  | jq -r ".Exports[] | select(.Name | startswith(\"ClientVpnEndpointId-${vpn_stack}\")).Value")

aws ec2 export-client-vpn-client-configuration\
  --client-vpn-endpoint-id ${client_vpn_endpoint} | jq -r '.ClientConfiguration' > client.ovpn

connect

Cognito demo

๐Ÿ“ make sure to create bucket and install requirements first

update bucket policy

โš ๏ธ public read access required for access to MetadataURL, adjust as necessary

tmpfile=$(mktemp)
cat << EOF > ${tmpfile}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "$(date +%s)",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::${bucket}/*"
      ]
    }
  ]
}
EOF

aws s3api put-bucket-policy\
  --bucket ${bucket}\
  --policy file://${tmpfile}\
  && rm ${tmpfile}

download metadata

copy metadata

domain_name='foo.bar'

aws s3 cp GoogleIDPMetadata-${domain_name}.xml s3://${bucket}/

package assets

pushd cognito-idp; for template in lambda main cognito; do
    aws cloudformation package\
      --template-file ${template}-template.yaml\
      --s3-bucket ${bucket}\
      --output-template-file ${template}.yaml
done; popd

deploy stack

stack_name='c0gn1t0-demo'
metadata_url=https://${bucket}.s3.amazonaws.com/GoogleIDPMetadata-${domain_name}.xml

pushd cognito-idp; aws cloudformation deploy\
  --template-file main.yaml\
  --stack-name ${stack_name}\
  --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM\
  --parameter-overrides\
  DomainName=${domain_name}\
  MetadataURL=${metadata_url}\
  --tags\
  Name=${stack_name}\
  Region=${AWS_REGION}\
  Profile=${AWS_PROFILE}\
  AccountId=$(aws sts get-caller-identity | jq -r '.Account'); popd


cognito_stack=$(aws cloudformation list-exports\
  | jq -r ".Exports[] | select(.Name==\"CognitoStackName-${stack_name}\").Value")

user_pool_id=$(aws cloudformation list-exports\
  | jq -r ".Exports[] | select(.Name | startswith(\"UserPoolId-${cognito_stack}\")).Value")


echo "ACS URL: https://${stack_name}.auth.${AWS_REGION}.amazoncognito.com/saml2/idpresponse"
echo "Entity ID: urn:amazon:cognito:sp:${user_pool_id}"

configure G Suite

Cognito IdP with Google SAML

VPC peering demo

creates a peering connection between source and destination VPCs, including tags and routes in both directions

package assets

pushd vpc-peering; for template in lambda main; do
    aws cloudformation package\
      --template-file ${template}-template.yaml\
      --s3-bucket ${bucket}\
      --output-template-file ${template}.yaml
done; popd

create IAM role

โ˜ข ensure appropriate VPCPeeringRole exists in the VPC accepter AWS account and review IAM role permissions

  VPCPeeringRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'VPCPeeringRole'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            AWS:
            # list your VPC peering requester (source) AWS accounts here
            - '123456789000'
            ...
          Action: sts:AssumeRole
      Path: '/'
      ...

update IAM role

โ˜ข add VPC requester AWS accounts to CustomResourceLambdaRole under the AmazonSTSPolicy policy and review IAM role permissions

  - PolicyName: AmazonSTSPolicy
    PolicyDocument:
      Version: '2012-10-17'
      Statement:
      - Effect: Allow
        Action:
        - 'sts:AssumeRole'
        - 'sts:PassRole'
        Resource:
        # list your VPC peering accepter (target) AWS accounts here
        - !Sub 'arn:${AWS::Partition}:iam::123456789001:role/VPCPeeringRole'
        ...

deploy stack

๐Ÿ“ optionally enable EC2 nested stack and supply SecurityGroup in the accepter VPC as well as TargetPort

# peering between VPCs in this mock account 123456789000 (requester) and 123456789001 (accepter)
stack_name='vpc-peering-demo'

# create IPv6 routes (both VPCs must be IPv6)
ipv6='false'

# requester VPC
source_vpc='vpc-abcdef1234567890'

# comma separated list of one or more route table id(s) in the requester VPC'
source_route_table_ids='rtb-abcdef1234567890'

# accepter VPC
source_vpc='vpc-1234567890abcdef'

# VPC accepter AWS account
target_account_id=123456789001

# VPC accepter AWS region
target_region=${AWS_REGION}

# comma separated list of one or more route table id(s) in the accepter VPC'
target_route_table_ids='rtb-1234567890abcdef'


source_route_table_ids=($(echo ${source_route_table_ids} | sed 's/,/ /g' | tr ' ' '\n'))\
  && source_route_tables=${#source_route_table_ids[@]}\
  && source_route_table_ids="$(echo ${source_route_table_ids[*]} | tr ' ' ',')"

target_route_table_ids=($(echo ${target_route_table_ids} | sed 's/,/ /g' | tr ' ' '\n'))\
  && target_route_tables=${#target_route_table_ids[@]}\
  && target_route_table_ids="$(echo ${target_route_table_ids[*]} | tr ' ' ',')"

pushd vpc-peering; aws cloudformation deploy\
  --template-file main.yaml\
  --stack-name ${stack_name}\
  --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM\
  --parameter-overrides\
  SourceVpcId=${source_vpc}\
  SourceRouteTableIds=${source_route_table_ids}\
  SourceRouteTables=${source_route_tables}\
  TargetRegion=${target_region}\
  TargetAccountId=${target_account_id}\
  TargetVpcId=${target_vpc}\
  TargetRouteTableIds=${target_route_table_ids}\
  TargetRouteTables=${target_route_tables}\
  EC2Template=false\
  --tags\
  Name=${stack_name}\
  Region=${AWS_REGION}\
  Profile=${AWS_PROFILE}\
  AccountId=$(aws sts get-caller-identity | jq -r '.Account'); popd

AWS Backup (EFS)

also see mock examples below

package assets

pushd aws-backup; for template in lambda; do
    aws cloudformation package\
      --template-file ${template}-template.yaml\
      --s3-bucket ${bucket}\
      --output-template-file ${template}.yaml
done; popd

deploy stack

see resource ARNs and namespaces for ResourceId parameter

stack_name='aws-backup-demo'

# specify resource ARN and name
resource_id="arn:aws:elasticfilesystem:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):file-system/fs-abcde1234"
resource_name='efs-resource-name'


pushd aws-backup; aws cloudformation deploy\
  --template-file backup.yaml\
  --stack-name ${stack_name}\
  --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM\
  --parameter-overrides\
  NameTag=${stack_name}\
  ResourceId=${resource_id}\
  ResourceName=${resource_name}\
  --tags\
  Name=${stack_name}\
  Region=${AWS_REGION}\
  Profile=${AWS_PROFILE}\
  AccountId=$(aws sts get-caller-identity | jq -r '.Account'); popd

mock client requests

๐Ÿž useful to debug resource creation of AWS resources from a local workstation

Backup

Backup API reference

create-backup-vault

mock CloudFormation request to create a backup vault

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"PhysicalResourceId\": \"$(uuid)\",
  \"ResourceProperties\": {
      \"AgentType\": \"client\",
      \"AgentService\": \"backup\",
      \"AgentCreateMethod\": \"create_backup_vault\",
      \"AgentDeleteMethod\": \"delete_backup_vault\",
      \"AgentCreateArgs\": {
          \"BackupVaultName\": \"foo-bar\",
          \"EncryptionKeyArn\": \"arn:aws:kms:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):key/$(uuid)\",
          \"BackupVaultTags\": {
            \"Name\": \"foo-bar\"
          }
      },
      \"AgentDeleteArgs\": {
          \"BackupVaultName\": \"foo-bar\"
      }
  }
}" | jq -c | VERBOSE=1 ./generic_provider.py
popd

create-backup-plan

mock CloudFormation request to create a backup plan

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
      \"AgentType\": \"client\",
      \"AgentService\": \"backup\",
      \"AgentCreateMethod\": \"create_backup_plan\",
      \"AgentUpdateMethod\": \"update_backup_plan\",
      \"AgentDeleteMethod\": \"delete_backup_plan\",
      \"AgentResourceId\": \"BackupPlanId\",
      \"AgentWaitQueryExpr\": \"$.BackupPlanId\",
      \"AgentCreateArgs\": {
        \"BackupPlan\": {
          \"BackupPlanName\": \"foo-bar\",
          \"Rules\": [
            {
              \"RuleName\": \"foo-bar\",
              \"TargetBackupVaultName\": \"Default\",
              \"ScheduleExpression\": \"cron(0 2 * * ? *)\",
              \"StartWindowMinutes\": 60,
              \"CompletionWindowMinutes\": 180,
              \"Lifecycle\": {
                \"MoveToColdStorageAfterDays\": 30,
                \"DeleteAfterDays\": 365
              }
            }
          ]
        },
        \"BackupPlanTags\": {
          \"Name\": \"foo-bar\"
        }
      },
      \"AgentUpdateArgs\": {
        \"BackupPlan\": {
          \"BackupPlanName\": \"foo-bar\",
          \"Rules\": [
            {
              \"RuleName\": \"foo-bar\",
              \"TargetBackupVaultName\": \"Default\",
              \"ScheduleExpression\": \"cron(0 2 * * ? *)\",
              \"StartWindowMinutes\": 60,
              \"CompletionWindowMinutes\": 180,
              \"Lifecycle\": {
                \"MoveToColdStorageAfterDays\": 30,
                \"DeleteAfterDays\": 365
              }
            }
          ]
        }
      }
    }
  }
}" | jq -c | VERBOSE=1 ./generic_provider.py
popd

create-backup-selection

mock CloudFormation request to create a backup slection

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
      \"AgentType\": \"client\",
      \"AgentService\": \"backup\",
      \"AgentCreateMethod\": \"create_backup_selection\",
      \"AgentDeleteMethod\": \"delete_backup_selection\",
      \"AgentResourceId\": \"SelectionId\",
      \"AgentWaitQueryExpr\": \"$.SelectionId\",
      \"AgentCreateArgs\": {
        \"BackupPlanId\": \"$(uuid)\",
        \"BackupSelection\": {
          \"SelectionName\": \"foo-bar\",
          \"IamRoleArn\": \"arn:aws:iam::$(aws sts get-caller-identity | jq -r '.Account'):role/service-role/AWSBackupDefaultServiceRole\",
          \"Resources\": [
            \"arn:aws:elasticfilesystem:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):file-system/fs-abcde1234\"
          ],
          \"ListOfTags\": [
            {
              \"ConditionType\": \"STRINGEQUALS\",
              \"ConditionKey\": \"AccountId\",
              \"ConditionValue\": \"$(aws sts get-caller-identity | jq -r '.Account')\"
            }
          ]
        }
      },
      \"AgentDeleteArgs\": {
        \"BackupPlanId\": \"$(uuid)\"
      }
    }
  }
}" | jq -c | VERBOSE=1 ./generic_provider.py
popd

Directory Services

Directory Services API reference

AD Connector

mock CloudFormation request to create AD Connector

mock_lambda_event=$(echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
    \"AgentService\": \"ds\",
    \"AgentType\": \"client\",
    \"AgentCreateMethod\": \"connect_directory\",
    \"AgentDeleteMethod\": \"delete_directory\",
    \"AgentWaitMethod\": \"describe_directories\",
    \"AgentWaitQueryExpr\": \"$.DirectoryDescriptions[].Stage\",
    \"AgentWaitCreateQueryValues\": [
        \"Active\"
    ],
    \"AgentWaitUpdateQueryValues\": [],
    \"AgentWaitDeleteQueryValues\": [],
    \"AgentResourceId\": \"DirectoryId\",
    \"AgentWaitResourceId\": [
      \"DirectoryIds\"
    ],
    \"AgentCreateArgs\": {
      \"Size\": \"Small\",
      \"Description\": \"Active Directory connection.\",
      \"Name\": \"foo-bar.local\",
      \"ShortName\": \"foo-bar\",
      \"Password\": \"bar\",
      \"ConnectSettings\": {
        \"VpcId\": \"vpc-abcdef1234567890\",
        \"SubnetIds\": [
          \"subnet-1234567890abcdef\",
          \"subnet-abcdef1234567890\"
        ],
        \"CustomerDnsIps\": [
          \"1.2.3.4\",
          \"4.5.6.7\"
        ],
        \"CustomerUserName\": \"foo\"
      }
    }
  }
}" | jq -c)\
&& pushd generic_provider\
&& ./generic_provider.py "${mock_lambda_event}"\
&& popd

IAM

IAM API reference

SSH public key

mock CloudFormation request to upload SSH public key

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
      \"AgentType\": \"client\",
      \"AgentService\": \"iam\",
      \"AgentResourceId\": \"SSHPublicKeyId\",
      \"AgentWaitQueryExpr\": \"$.SSHPublicKey.SSHPublicKeyId\",
      \"AgentCreateMethod\": \"upload_ssh_public_key\",
      \"AgentCreateArgs\": {
          \"UserName\": \"foo-bar\",
          \"SSHPublicKeyBody\": \"$(cat ~/.ssh/id_rsa.pub | head -n 1)\"
      },
      \"AgentDeleteMethod\": \"delete_ssh_public_key\",
      \"AgentDeleteArgs\": {
        \"UserName\": \"foo-bar\"
      }
  }
}" | jq -c | ./generic_provider.py
popd

KMS

KMS API reference

encrypt

mock CloudFormation request to encrypt with KMS

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
      \"AgentCreateArgs\": {
          \"KeyId\": \"arn:aws:kms:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):key/$(uuid)\",
          \"Plaintext\": \"foo-bar\"
      },
      \"AgentType\": \"client\",
      \"AgentService\": \"kms\",
      \"AgentCreateMethod\": \"encrypt\"
  }
}" | jq -c | ./generic_provider.py
popd

Relational Database Service

RDS API reference

modify-db-cluster

mock CloudFormation request to enable RDS CloudWatch metrics

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"PhysicalResourceId\": \"$(uuid)\",
  \"ResourceProperties\": {
    \"AgentService\": \"rds\",
    \"AgentType\": \"client\",
    \"AgentCreateMethod\": \"modify_db_cluster\",
    \"AgentCreateArgs\": {
      \"DBClusterIdentifier\": \"foo-bar\",
      \"CloudwatchLogsExportConfiguration\": {
        \"EnableLogTypes\": [
          \"error\",
          \"slowquery\"
        ],
        \"DisableLogTypes\": []
      }
    },
    \"AgentDeleteMethod\": \"modify_db_cluster\",
    \"AgentDeleteArgs\": {
      \"DBClusterIdentifier\": \"foo-bar\",
      \"CloudwatchLogsExportConfiguration\": {
        \"DisableLogTypes\": [
          \"error\",
          \"slowquery\"
        ],
        \"EnableLogTypes\": []
      }
    },
    \"AgentWaitMethod\": \"describe_db_instances\",
    \"AgentWaitDelay\": \"60\",
    \"AgentWaitArgs\": {
      \"Filters\": [
        {
          \"Name\": \"db-cluster-id\",
          \"Values\": [
            \"foo-bar\"
          ]
        },
        {
          \"Name\": \"db-instance-id\",
          \"Values\": [
            \"foo-bar\"
          ]
        }
      ]
    },
    \"AgentWaitQueryExpr\": \"$.DBInstances[*].DBInstanceStatus\",
    \"AgentWaitCreateQueryValues\": [
        \"available\"
    ],
    \"AgentWaitDeleteQueryValues\": [
        \"available\"
    ]
  }
}" | jq -c | ./generic_provider.py
popd

modify-db-instance

mock CloudFormation request to enable RDS Performance Insights

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"PhysicalResourceId\": \"$(uuid)\",
  \"ResourceProperties\": {
    \"AgentService\": \"rds\",
    \"AgentType\": \"client\",
    \"AgentCreateMethod\": \"modify_db_instance\",
    \"AgentCreateArgs\": {
      \"DBInstanceIdentifier\": \"abcdefghij1234\",
      \"EnablePerformanceInsights\": true,
      \"PerformanceInsightsKMSKeyId\": \"arn:aws:kms:${AWS_REGION}:1234567890:key/$(uuid)\",
      \"PerformanceInsightsRetentionPeriod\": 7,
      \"ApplyImmediately\": true
    },
    \"AgentDeleteMethod\": \"modify_db_instance\",
    \"AgentDeleteArgs\": {
      \"DBInstanceIdentifier\": \"1234abcdefghij\",
      \"EnablePerformanceInsights\": false,
      \"ApplyImmediately\": true
    },
    \"AgentWaitMethod\": \"describe_db_instances\",
    \"AgentWaitDelay\": \"60\",
    \"AgentWaitArgs\": {
      \"Filters\": [
        {
          \"Name\": \"db-cluster-id\",
          \"Values\": [
            \"1234abcdefghij\"
          ]
        },
        {
          \"Name\": \"db-instance-id\",
          \"Values\": [
            \"abcdefghij1234\"
          ]
        }
      ]
    },
    \"AgentWaitQueryExpr\": \"$.DBInstances[*].DBInstanceStatus\",
    \"AgentWaitCreateQueryValues\": [
        \"available\"
    ],
    \"AgentWaitDeleteQueryValues\": [
        \"available\"
    ]
  }
}" | jq -c | ./generic_provider.py
popd

Database Migration Service

DMS API reference

describe-replication-tasks

mock CloudFormation request to describe running replication tasks

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
    \"AgentService\": \"dms\",
    \"AgentType\": \"client\",
    \"AgentCreateMethod\": \"describe_replication_tasks\",
    \"AgentCreateArgs\": {
      \"Filters\": [
        {
          \"Name\": \"replication-instance-arn\",
          \"Values\": [
            \"arn:aws:dms:us-west-2:313347522657:rep:ABCDEFGHIJKLMNOPQRSTUVWXYZ\"
          ]
        }
      ]
    },
    \"AgentWaitQueryExpr\": \"$.ReplicationTasks[?(@.Status=='running')].ReplicationTaskArn\"
  }
}" | jq -c | ./generic_provider.py
popd

stop-replication-task

mock CloudFormation request to stop replication task

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
    \"AgentService\": \"dms\",
    \"AgentType\": \"client\",
    \"AgentWaitMethod\": \"replication_task_stopped\",
    \"AgentWaitArgs\": {
      \"Filters\": [
        {
          \"Name\": \"replication-task-arn\",
          \"Values\": [
            \"arn:aws:dms:${AWS_REGION}:1234567890:task:ABCDEFGHIJKLMNOPQRSTUVWXYZ\"
          ]
        }
      ]
    },
    \"AgentCreateMethod\": \"stop_replication_task\",
    \"AgentCreateExceptions\": [
      \"agent.exceptions.InvalidResourceStateFault\",
      \"agent.exceptions.ClientError\"
    ],
    \"AgentWaitCreateExceptions\": [
      \"botocore.exceptions.WaiterError\"

    ],
    \"AgentCreateArgs\": {
      \"ReplicationTaskArn\": \"arn:aws:dms:${AWS_REGION}:1234567890:task:ABCDEFGHIJKLMNOPQRSTUVWXYZ\"
    }
  }
}" | jq -c | ./generic_provider.py
popd

EC2

EC2 API reference

create-tags

mock CloudFormation request to tag resources

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
      \"AgentType\": \"client\",
      \"AgentService\": \"ec2\",
      \"AgentCreateMethod\": \"create_tags\",
      \"AgentCreateArgs\": {
          \"Resources\": [
              \"eipalloc-12345677890\"
          ],
          \"Tags\": [
              {
                  \"Key\": \"foo\",
                  \"Value\": \"bar\"
              }
          ]
      },
      \"AgentDeleteMethod\": \"delete_tags\",
      \"AgentDeleteArgs\": {
          \"Resources\": [
              \"eipalloc-12345677890\"
          ],
          \"Tags\": [
              {
                  \"Key\": \"foo\",
                  \"Value\": \"bar\"
              }
          ]
      }
  }
}" | jq -c | ./generic_provider.py
popd

authorize-security-group-ingress

mock CloudFormation request to authorize_security_group_ingress in another account

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
      \"AgentType\": \"client\",
      \"AgentService\": \"ec2\",
      \"RoleArn\": \"arn:aws:iam::1234567890:role/CrossAccountRole\",
      \"AgentRegion\": \"us-east-1\",
      \"AgentCreateMethod\": \"authorize_security_group_ingress\",
      \"AgentCreateArgs\": {
          \"GroupId\": \"sg-1234567890abcdef\",
          \"IpPermissions\": [
              {
                  \"FromPort\": 22,
                  \"IpProtocol\": \"tcp\",
                  \"IpRanges\": [
                      {
                          \"CidrIp\": \"172.16.0.0/16\",
                          \"Description\": \"foo-bar\"
                      }

                  ],
                  \"ToPort\": 22
              }
          ]
      },
      \"AgentDeleteMethod\": \"revoke_security_group_ingress\",
      \"AgentDeleteArgs\": {
          \"GroupId\": \"sg-1234567890abcdef\",
          \"IpPermissions\": [
              {
                  \"FromPort\": 22,
                  \"IpProtocol\": \"tcp\",
                  \"IpRanges\": [
                      {
                          \"CidrIp\": \"172.16.0.0/16\",
                          \"Description\": \"foo-bar\"
                      }

                  ],
                  \"ToPort\": 22
              }
          ]
      }
  }
}" | jq -c | ./generic_provider.py
popd

modify-subnet-attribute

mock CloudFormation request to modify subnet attribute(s)

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
      \"AgentType\": \"client\",
      \"AgentService\": \"ec2\",
      \"AgentCreateMethod\": \"modify_subnet_attribute\",
      \"AgentCreateArgs\": {
          \"MapPublicIpOnLaunch\": {
              \"Value\": true
          },
          \"SubnetId\": \"subnet-abcdef1234567890\"
      }
  }
}" | jq -c | ./generic_provider.py
popd

get-parameter

mock CloudFormation request to get existing SSM parameter (stored outside of stack)

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
      \"AgentType\": \"client\",
      \"AgentService\": \"ssm\",
      \"AgentCreateMethod\": \"get_parameter\",
      \"AgentWaitQueryExpr\": \"$.Parameter.Value\",
      \"AgentCreateArgs\": {
          \"Name\": \"/foo/bar\",
          \"WithDecryption\": true
      }
  }
}" | jq -c | ./generic_provider.py
popd

put-parameter

mock CloudFormation request to put SSM parameter

pushd generic_provider
echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
      \"AgentType\": \"client\",
      \"AgentService\": \"ssm\",
      \"AgentCreateMethod\": \"put_parameter\",
      \"AgentUpdateMethod\": \"put_parameter\",
      \"AgentDeleteMethod\": \"delete_parameter\",
      \"AgentResourceId\": \"Name\",
      \"AgentCreateArgs\": {
          \"Name\": \"/foo/bar\",
          \"Value\": \"foo-bar\",
          \"Type\": \"SecureString\",
          \"Overwrite\": false
      },
      \"AgentUpdateArgs\": {
          \"Name\": \"/foo/bar\",
          \"Value\": \"foo-bar\",
          \"Type\": \"SecureString\",
          \"Overwrite\": true
      },
      \"AgentDeleteArgs\": {
          \"Name\": \"/foo/bar\"
      }
  }
}" | jq -c | ./generic_provider.py
popd

mock resources requests

EC2 API reference

network-interfaces-attribute

mock CloudFormation request to obtain instance public IPv6 address

 pushd generic_provider
 echo "{
  \"RequestType\": \"Create\",
  \"ResponseURL\": \"https://cloudformation-custom-resource-response-${AWS_REGION}.s3.amazonaws.com/\",
  \"StackId\": \"arn:aws:cloudformation:${AWS_REGION}:$(aws sts get-caller-identity | jq -r '.Account'):stack/MockStack/$(uuid)\",
  \"RequestId\": \"$(uuid)\",
  \"ResourceType\": \"Custom::MockResource\",
  \"LogicalResourceId\": \"MockResource\",
  \"ResourceProperties\": {
    \"AgentService\": \"ec2\",
    \"AgentType\": \"resource\",
    \"AgentWaitQueryExpr\": \"$..Ipv6Address\",
    \"AgentResourceId\": \"Ipv6Address\",
    \"AgentCreateMethod\": \"network_interfaces_attribute\",
    \"AgentCreateArgs\": {
      \"ResourceName\": \"Instance\",
      \"ResourceId\": \"i-abcdef1234567890\"
    }
  }
}" | jq -c | ./generic_provider.py
popd

โ€“belodetek ๐Ÿ˜ฌ