aws-cdk icon indicating copy to clipboard operation
aws-cdk copied to clipboard

(ecr): Code.from_ecr_image tag_or_digest using CFNParameter

Open bhsp opened this issue 2 years ago • 8 comments

Describe the bug

When creating a Lambda utilizing code from ECR, the tag_or_digest works great if the value is literally the tag or sha256: digest. This falls down when providing a CFNParameter as it's value but only when the user input is a digest, works fine with a tag still.

For example. tag_or_digest='sha256:a5ecdfb1bd870e9aa68d3921768cd2d4866be34bac3e41503f2c3c0b6db5a167' generates the following YML - which of course works and is use the @ correctly.

lambdafunction841552AF:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ImageUri:
          Fn::Join:
            - ""
            - - 111111111111.dkr.ecr..
              - Ref: AWS::URLSuffix
              - /test/test@sha256:a5ecdfb1bd870e9aa68d3921768cd2d4866be34bac3e41503f2c3c0b6db5a167

Using a tag, also works great! tag_or_digest='latest'

  lambdafunction841552AF:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ImageUri:
          Fn::Join:
            - ""
            - - 111111111111.dkr.ecr..
              - Ref: AWS::URLSuffix
              - /test/test:latest

If/when we use a CfnParameter and during deploy pass a sha256: the Intrinsic join uses a ":" instead of the "@".


Parameters:
  version:
    Type: String
    AllowedPattern: ^([a-zA-Z0-9_][a-zA-Z0-9_.-]{1,127}|sha256:[0-9a-f]{64})$
    ConstraintDescription: Must match pattern ^([a-zA-Z0-9_][a-zA-Z0-9_.-]{1,127}|sha256:[0-9a-f]{64})$
    Description: Image tag e.g., 'latest' or a digest beginning with sha256:, please verify the digest exists in the repository.
    MaxLength: 127
    MinLength: 1
    NoEcho: false

  lambdafunction841552AF:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ImageUri:
          Fn::Join:
            - ""
            - - 111111111111.dkr.ecr..
              - Ref: AWS::URLSuffix
              - "/test/test:"
              - Ref: version

Expected Behavior

URL is joined using an @ when a digest is provided through a CfnParameter

Current Behavior

The ECR URI is joined by a colon instead of an @ when a digest is provided.

Reproduction Steps

#!/usr/bin/env python3
from constructs import Construct
from aws_cdk import App, Environment, Stack, Duration, CfnParameter
from aws_cdk import (aws_lambda as lambda_, aws_ecr as ecr)


class LambdaStack(Stack):

    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        pattern = '^([a-zA-Z0-9_][a-zA-Z0-9_.-]{1,127}|sha256:[0-9a-f]{64})$'

        lambda_.Function(
            self,
            "lambda-function",
            function_name='lambda-ecr-image',
            runtime=lambda_.Runtime.FROM_IMAGE,
            architecture=lambda_.Architecture.X86_64,
            handler=lambda_.Handler.FROM_IMAGE,
            # Maximum is 15 minutes
            timeout=Duration.minutes(14),
            retry_attempts=1,
            code=lambda_.Code.from_ecr_image(
                repository=ecr.Repository.from_repository_arn(
                    self,
                    id="private-repo",
                    repository_arn="arn:aws:ecr::111111111111:repository/test/test"
                ),

                # Works with the digest as a value
                # Works with the tag as a value
                # Does not work with digest through CfnParameter
                tag_or_digest=CfnParameter(
                    self,
                    id="version",
                    description=f"Image tag e.g., 'latest' or a digest beginning with sha256:, "
                                f"please verify the digest exists in the repository.",
                    allowed_pattern=pattern,
                    constraint_description='Must match pattern ' + pattern,
                    max_length=127,
                    min_length=1,
                    no_echo=False,
                    type="String"
                ).value_as_string
            )
        )


env = Environment(account="111111111111", region='us-east-1')
app = App()

lambda_stack = LambdaStack(app, "lambda-stack", env=env)

app.synth()

Possible Solution

Intrinsic conditional join

Additional Information/Context

No response

CDK CLI Version

2.22.0 (build 1db4b16)

Framework Version

No response

Node.js Version

14.19.1

OS

Windows

Language

Python

Language Version

3.8

Other information

No response

bhsp avatar May 04 '22 17:05 bhsp

Thanks for opening this issue @bhsp! Looks like this is more of an ecr bug than a lambda one. The offending line should be this:

https://github.com/aws/aws-cdk/blob/0d74ff64e2b5730a4cb1e1fa1d32806782ebf4e5/packages/%40aws-cdk/aws-ecr/lib/repository.ts#L183

We're not really considering CfnParameters and then just defaulting to :. It's not possible to actually tell what your CfnParameter is at synth time, so the likely solution here is to add an optional parameter to EcrImage to force the digest option. It's not pretty, but I don't see a better way.

kaizencc avatar May 10 '22 17:05 kaizencc

This would be great, I'd 100% prefer to be explicit when building the ECR image URI for a lambda using a CFN parameter to force the digest option.

bhsp avatar May 11 '22 13:05 bhsp

If have been fiddling with this and got something working.

Here is the code for repositoryUriForTagOrDigest

  public repositoryUriForTagOrDigest(tagOrDigest?: string): string {
    if(Token.isUnresolved(tagOrDigest)) {
      const condition = new CfnCondition(this, `TagCondition`, {
        expression: Fn.conditionAnd(
          Fn.conditionNot(Fn.conditionEquals(tagOrDigest, '')),
          Fn.conditionEquals(Fn.select(0, Fn.split('sha256:', tagOrDigest!)), '')
        ),
      });
      return Fn.conditionIf(
        condition.logicalId,
        this.repositoryUriForDigest(tagOrDigest),
        this.repositoryUriForTag(tagOrDigest)).toString();
    }
    if (tagOrDigest?.startsWith('sha256:')) {
      return this.repositoryUriForDigest(tagOrDigest);
    } else {
      return this.repositoryUriForTag(tagOrDigest);
    }
  }

The resulting template from a test:

{
          "Conditions": {
              "ScanRepoTagConditionF42E0720": {
                  "Fn::And": [
                      {
                          "Fn::Not": [
                              {
                                  "Fn::Equals": [
                                      {
                                          "Ref": "param"
                                      },
                                      ""
                                  ]
                              }
                          ]
                      },
                      {
                          "Fn::Equals": [
                              {
                                  "Fn::Select": [
                                      0,
                                      {
                                          "Fn::Split": [
                                              "sha256:",
                                              {
                                                  "Ref": "param"
                                              }
                                          ]
                                      }
                                  ]
                              },
                              ""
                          ]
                      }
                  ]
              }
          },
          "Parameters": {
              "param": {
                  "Type": "String"
              }
          },
          "Outputs": {
              "RepoUri": {
                  "Value": {
                      "Fn::If": [
                          "ScanRepoTagConditionF42E0720",
                          {
                              "Fn::Join": [
                                  "",
                                  [
                                      "111111111111.dkr.ecr..",
                                      {
                                          "Ref": "AWS::URLSuffix"
                                      },
                                      "/test/test@",
                                      {
                                          "Ref": "param"
                                      }
                                  ]
                              ]
                          },
                          {
                              "Fn::Join": [
                                  "",
                                  [
                                      "111111111111.dkr.ecr..",
                                      {
                                          "Ref": "AWS::URLSuffix"
                                      },
                                      "/test/test:",
                                      {
                                          "Ref": "param"
                                      }
                                  ]
                              ]
                          }
                      ]
                  }
              }
          }
      }

I have a few doubts:

  • how to make sure the condition is only added once during the phases?
  • is it possible to prevent name clashes with other conditions in the stack?
  • can we be sure the stack I create the condition on is always the correct one (it uses the stack of the ECR)?
  • the tagOrDigest! might be a little unsafe
  • is this worth the effort (I had fun making this anyway ;-))
  • if think the unresolved must be a parameter (not something else) to be ref-ed in a condition, can we distinguish?

Credits for the Fn::StartsWith condition: https://dev.to/lambdasharp/emulating-fn-startswith-in-cloudformation-756

The unit test I used:


  test('test repositoryUriForTagOrDigest with CfnParameter', () => {
    // GIVEN
    const stack = new cdk.Stack();
    const repository = ecr.Repository.fromRepositoryArn(stack, 'ScanRepo', "arn:aws:ecr::111111111111:repository/test/test");
    const param = new cdk.CfnParameter(stack, 'param', {
      type: "String"
    });

    // WHEN
    new cdk.CfnOutput(stack, 'RepoUri', {
      value: repository.repositoryUriForTagOrDigest(param.valueAsString)
    });

    // THEN
    console.log(JSON.stringify(Template.fromStack(stack), null, 4));
});

Curious what you think :-) Let me know if you want me to cook up a PR of this.

Jacco avatar May 13 '22 14:05 Jacco

@Jacco This is great! I plan to get back to this next week, thank you.

bhsp avatar Jun 06 '22 14:06 bhsp

@Jacco This is great! I plan to get back to this next week, thank you.

@bhsp We will have to find someone (@kaizencc ?) to review/approve the solution direction.

Jacco avatar Jun 06 '22 17:06 Jacco

Is there any progress on this? We just found this issue and got blocked on it. I'm still looking for a workaround but there is nothing obvious with the way the code is structured right now.

angarg12 avatar Jun 19 '23 17:06 angarg12

Any update on this? @kaizencc

JZechy avatar Feb 20 '24 15:02 JZechy

Until this moves forward. I found a workaround for this. Since CfnParameter is resolved at the template execution, we can use template functions Fn::Split & Fn::Select.

Here's the workaround in the C#. Simply prefix the token value with sha256:, which will force CDK to parse the value as digest reference, an through the template functions, get rid off that part once the value is resolved in CloudFormation. Works like a charm!

CfnParameter imageDigest = new(this, "ImageDigest", new CfnParameterProps
{
    Type = "String"
});

string parsedImageDigest = Fn.Select(1, Fn.Split("sha256:", imageDigest.ValueAsString));

new DockerImageFunction(this, "docker", new DockerImageFunctionProps
{
    Code = DockerImageCode.FromEcr(repository, new EcrImageCodeProps
    {
        TagOrDigest = "sha256:" + parsedImageDigest
    })
});

Resulting template will looks like this:

apifunction2E1B8850:
  Type: AWS::Lambda::Function
  Properties:
    Code:
      ImageUri:
        Fn::Join:
          - ""
          - - 112233445566.dkr.ecr.eu-central-1.
            - Ref: AWS::URLSuffix
            - "image@sha256:"
            - Fn::Select:
                - 1
                - Fn::Split:
                    - "sha256:"
                    - Ref: ImageDigest

JZechy avatar Feb 20 '24 17:02 JZechy