Skip to content

Instantly share code, notes, and snippets.

@dmost714
Last active October 21, 2023 14:22
Show Gist options
  • Save dmost714/e914c865e54f9e77a1e49cda3dc78b20 to your computer and use it in GitHub Desktop.
Save dmost714/e914c865e54f9e77a1e49cda3dc78b20 to your computer and use it in GitHub Desktop.
How to expose an Amplify Custom category as an ENVIRONMENT variable in an Amplify Lambda

How to expose an Amplify Custom category as an ENVIRONMENT variable in an Amplify Lambda

These are notes I wrote to myself, but if they help you please leave a note and let me know. And any tips are certainly appreciated.

TL;DR

To reference your custom category resource from your lambda:

  1. amplify add custom to make custom category "XXX"
  2. Add Outputs to XXX-cloudformation-template.json
  3. Inside backend-config.json add a dependsOn to the lambda.
"yourLambdaFunction": {
  "build": true,
  "providerPlugin": "awscloudformation",
  "service": "Lambda",
  "dependsOn": [
    {
      "category": "custom",
      "resourceName": "XXX",
      "attributes": [
        "var name from Outputs of XXX-cloudformation-template.json",
      ]
    }
  ]
}
  1. Reference attribures from step 3 in the Parameters of the lambda (amplify/backend/function/yourLambda/yourLambda-cloudformation-template.json)
  2. Name each parameter by mashing up custom, the name of your custom category (XXX) and the name of the attribute from step 3. e.g. customXXXMyResourceArn
  3. Add the environment variable
  "Resources": {
    "LambdaFunction": {
      "Type": "AWS::Lambda::Function",
        "Environment": {
          "Variables": {
            "NAME_OF_ENV_VARIABLE": {
              "Ref": "customXXXMyResourceArn"
            }
          }
        }
      }
    }
  }
  1. Use the environment variable in your code.
console.log('NAME_OF_ENV_VARIABLE', process.env.NAME_OF_ENV_VARIABLE)
  1. Profit

Add your custom resource to Amplify.

amplify add custom

If you call your new resource "XXX" you'll end up with a custom directory containing an XXX directory that contains the XXX-cloudformation-template.json file.

Modify the cloudformation-template file to add your custom resources. In this example, the resource is called "YYY". The Type and Properties values depend on what type of resource you're adding.

  "Resources": {
    "YYY": {
      "Type": "AWS::SQS::Queue",
      "Properties": {
        ...
      }
    }
  },

Sidebar: Getting Name, Arn, and other attributes of your resource

When YYY is defined/created, it will expose multiple values, which you can discover in the CloudFormation documentation Just search for cloudformation followed by the value of the resource type. e.g. cloudformation AWS::SQS::Queue, scroll past the "Properties" section to the "Return values" section.

You can incorporate those return values in other places within CloudFormation by using "Ref" and "Fn:GetAtt".

For example, if you have an SQS Queue and its Dead Letter Queue, the primary queue needs the Arn of the DLQ. You get this using Fn::GetAtt where the 1st value is the name of the resource in the Properties object (SQSDLQ) and the 2nd value is the attribute you need (Arn).

"deadLetterTargetArn": {
  "Fn::GetAtt": [
    "SQSDLQ",
    "Arn"
  ]
}

Expose custom resource attributes to other categories and resources

Update the Outputs object in your XXX-cloudformatino-template.json file with whatever values you want to expose. Typically these will be Name and/or Arn.

In this example, ZZZ is the name of the output variable you're exposing and YYY is the resource.

  "Outputs": {
    "ZZZ": {
      "Value": {
        "Ref": "YYY"
      }
    }
  }

Remember, the Value represented by ZZZ in this example is whatever the Ref is for the ZZZ resource. You might need to expose a different attribute or multiple attributes, so your Outputs may look like this:

  "Outputs": {
    "ZZZUrl": {
      "Value": {
        "Ref": "YYY"
      }
    },
    "ZZZArn": {
      "Value": {
        "Fn::GetAtt": [
          "YYY",
          "Arn"
        ]
      }
    }
  }

Because Ref for resource type ZZZ returns the url for the resource. And you also need the Arn of the resource, which you get via the Fn::GetAtt (RRRRemember there's no R in GetAtt) helper.

The name of each output variable can be whatever you like, but it's helpful to incorporate the name of the resource and the attribute you're exposing.

Deploy and Verify

Now your custom category is created. Push it to the cloud using amplify push, head over to CloudFormation "Stacks", search for "customXXX" where "XXX" is the name of your custom resource, and drill into that stack.

In the "Outputs" tab, you should see a Key for each of the variables you exported, and its value. Eyeball everything to be sure the value is what you expect -- the resources name, Arn, a Url, etc.

An existing amplify resource

If the resource that needs your custom resource doesn't exist yet, create it now. e.g. amplify function add to make a new Lambda.

Let's call the resource that wants access to your custom resource "WWW".

You'll be asked if you want to access other resources, and you'll see "custom" in the list and your custom resource. Hazah! Unfortunately, that doesn't help you connect the outputs or you wouldn't need this guide.

Recap

As a recap, here are "names" we've been using:

  • WWW = some amplify resource (e.g. lambda)
  • XXX = your custom category
  • YYY = your custom resource
  • ZZZ = YYY output variable to use

Amplify Magic Link

Inside the backend-config.json file, add an object to the dependsOn list of the resource (WWW) that needs access to your custom resource (YYY). The resourceName value is the name of the folder containing your custom catagory (XXX).

Here: /amplify/backend/custom/XXX/XXX-cloudformatino-template.json ... the resourceName is XXX.

EXAMPLE:

File: amplify/backend/backend-config.json

{
  "auth": {...},
  "storage": {...},
  "api": {...},
  "function": {
    "WWW": {
      "build": true,
      "providerPlugin": "awscloudformation",
      "service": "Lambda",
      "dependsOn": [
        {
          "category": "custom",
          "resourceName": "XXX",
          "attributes": [
            "YYYRef",
            "YYYArn"
          ]
        }
      ]
    }
  },
  "custom": {
    "XXX": {
      "service": "customCloudformation",
      "providerPlugin": "awscloudformation",
      "dependsOn": []
    }
  }
}

Above WWW is the name of the lambda (resource) that wants access to the custom resource. Within its dependsOn list, add an entry for your custom category XXX, and the values (YYYRef, YYYArn) from the Outputs section of your custom resource that you defined in /amplify/backend/custom/XXX/XXX-cloudformatino-template.json.

Pull the custom resource into another resource

Head over to the resource (e.g. the WWW lambda) that wants to reference the custom resource (e.g. XXX's YYY).

Add Parameters inside the /amplify/backend/lambda/WWW/WWW-cloudformation-template.json file (or whichever resource you want to reference the custom category).

Each parameter has a "magic" name. The name is a mashup of the word "custom" followed by the name of your custom category (XXX) followed by the name of the value from the Outputs section of your XXX-cloudformation-template.json file (which is also the same name you used in the dependsOn attributes section).

Putting it all together, the name used in Parameters is customXXXYYYattr where "YYYattr" is the name of the resource attribute.

The Parameters section probably already has a few values in it. Adding the new parameters it should look like this:

  "Parameters": {
    "CloudWatchRule": {
      "Type": "String",
      "Default": "NONE",
      "Description": " Schedule Expression"
    },
    "deploymentBucketName": {
      "Type": "String"
    },
    "env": {
      "Type": "String"
    },
    "s3Key": {
      "Type": "String"
    },
    "customXXXYYYRef": {
      "Type": "String"
    },
    "customXXXYYYArn": {
      "Type": "String"
    }
  },

Reference the custom resource

Now you can reference your custom resources's output value via {"Ref": "customXXXYYYArn"}.

Let's say the name of the custom category you created is called "SQS" (we've been using XXX for this) and you're using it to hold all your SQS queues. One of your queues (YYY) is called "OrdersQueue". In order to make the Lambda trigger when messages are added to the OrdersQueue, you add an "EventSourceMapping".

The "Easy" way to do this would be to put this mapping in the resources section of the SQS-cloudformation-template.json file, like this:

    "LambdaSQSEventSource": {
      "Type": "AWS::Lambda::EventSourceMapping",
      "Properties": {
        "Enabled": true,
        "EventSourceArn": {
          "Fn::GetAtt": [
            "OrdersQueue",
            "Arn"
          ]
        },
        "FunctionName": {
          "Ref": "functionNameOfYourLambdaName"
        },
        "FunctionResponseTypes": [
          "ReportBatchItemFailures"
        ]
      }
    }

But let's say you like pain. You can put the definition into the NameOfYourLambda-cloudformation-template.json file and use the Arn of your queue that you exported from your custom resource. Like so:

    "LambdaSQSEventSource": {
      "Type": "AWS::Lambda::EventSourceMapping",
      "Properties": {
        "Enabled": true,
        "EventSourceArn": {
          "Ref": "customSQSOrdersQueueArn"
        },
        "FunctionName": {
          "Fn::GetAtt": [
            "LambdaFunction",
            "Arn"
          ]
        },
        "FunctionResponseTypes": [
          "ReportBatchItemFailures"
        ]
      }
    }

Where customSQSOrdersQueueArn is also an entry in the Parameters section so you can reference it.

Access values from your Lambda code

Finally, we close the circle by exposing information about your custom category's resources to your Lambda's code.

In your Lambda's cloudformation template (NameOfYourLambda-cloudformation-template.json) find the LambdaFunction definition and add your environment variable.

  "Resources": {
    "LambdaFunction": {
      "Type": "AWS::Lambda::Function",
      "Metadata": {...},
      "Properties": {...},
        "Handler": "index.handler",
        "FunctionName": {...},
        "Environment": {
          "Variables": {
            "ENV": {
              "Ref": "env"
            },
            "REGION": {
              "Ref": "AWS::Region"
            },
            "NAME_OF_YOUR_ENV_VARIABLE": {
              "Ref": "customXXXYYYattr"
            },
            "SQS_ORDERS_QUEUE_URL": {
              "Ref": "customSQSOrdersQueueUrl"
            }
          }
        },
        "Role": {...},
        "Runtime": "nodejs16.x",
        "Layers": [],
        "Timeout": 30
      }
    },

Within your code, you can access it...

if (undefined === process.env.NAME_OF_YOUR_ENV_VARIABLE) {
  throw new Error("NAME_OF_YOUR_ENV_VARIABLE is required!")
}
console.log("NAME_OF_YOUR_ENV_VARIABLE", NAME_OF_YOUR_ENV_VARIABLE)

amplify category update

Beware: If you amplify function update and make changes, edits you made to the build-config.json file in the updated category will (at time of this writing) be wiped out when the new dependsOn key is made. Be sure to restore the "category": "custom" you made or you'll get cloudformation errors when you push.

Naming Things

I like to name the (custom) category with a descriptive (about what the category does or how it's used) term, and the resources within the category simply by their resource type.

Example: SNS Topic

I made a custom category to send emails to admins on serious errors. The custom category name is "AlertNotice". The reources in the category are simply "SNSTOPIC" and SNSSUBSCRIPTION_PERSON1, SNSSUBSCRIPTION_PERSON2 The output is "SNSTopicArn". In lambdas, this

  dependsOn:
    category: custom
    resourceName: AlertNotice
    attriburtes: SNSTopicArn
  ... in the cloudformation template becomes: customAlertNoticeSNSTopicArn
  ... then as an environment var: ALERT_NOTICE_SNS_TOPIC_ARN

Example: SQS

An SQS queue with dead letter queue, cloudwatch alarm The custom category name is "Pipeline". The reources in the category are simply "SQS", "DLQ", and "DLQALARM". The outputs are "SQSUrl" and "SQSArn".

In lambdas, this

  dependsOn:
    category: custom
    resourceName: Pipeline
    attriburtes: SQSUrl, SQSArn
  ... in the cloudformation template becomes: customPipelineSQSUrl
  ... then as an environment var: PIPELINE_SQS_URL

misc

If you need to reference Amplify resources (defined in other -cloudformation-template.json files), you can use the amplify update custom command to pull references to those resources into your custom cloudformation template. After running that commend, you will have a bunch of Parameters at the top of your custom cloudformation-template file and simply use Ref to incorporate them. For example: {"Ref": "name of parameter"}

Unfortunately, when you amplify function update and indicate you want to reference thigns in your custom category, the outputs you added aren't pulled in, so you need to add them manually.

Pro tip: any time you modify backend-config.json, you should run amplify env checkout dev (or whatever env you're currently using for development). This seems to rekajigger things. Then you can run amplify push -y.

Pro tip: sketch out how your resources will interact to avoid creating circular dependencies. It's tedious to shift things around later. If you do need to move something, 1st delete it and push an update, then add it back in the new .cloudformation-template file.

@dmost714
Copy link
Author

You can reference the output vars in the custom-policies.json file of a Lambda function that uses the new custom resource. This causes Amplify to automatically generate the permissions your lambda will need and add them to your cloud watch templates.

[
  {
    "Action": [
      "SQS:SendMessage",
      "SQS:ReceiveMessage",
      "SQS:DeleteMessage",
      "SQS:GetQueueAttributes"
    ],
    "Resource": [
      {
        "Ref": "customPipelineSQSArn"
      },
      {
        "Ref": "customSFTPSQSArn"
      }
    ]
  },
  {
    "Action": [
      "SNS:Publish"
    ],
    "Resource": [
      {
        "Ref": "customAlertNoticeSNSTopicArn"
      }
    ]
  }
]

@dmost714
Copy link
Author

And if you get cloudformation errors on push, e.g. remember that running amplify env checkout dev any time the build-config.json file is touched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment