aws-cdk
aws-cdk copied to clipboard
(ecr): Code.from_ecr_image tag_or_digest using CFNParameter
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
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.
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.
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 This is great! I plan to get back to this next week, thank you.
@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.
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.
Any update on this? @kaizencc
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