# Infrastructure as Code (CloudFormation and CDK) The AWS CLI (`aws lambda-web`) is the quickest path — see [deployment.md](references/deployment.md) and the CLI section of `SKILL.md`. Use CloudFormation when you want infrastructure-as-code (IaC): repeatable deployments, rollback support, and resources managed as a stack. Load this file only when the user asks about CloudFormation or infrastructure-as-code for Lambda Web Functions. **Check the current documentation before creating any CDK or SAM code:** the [AWS SAM resource reference](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-resources-and-properties.html) and the [AWS CDK API reference](https://docs.aws.amazon.com/cdk/api/v2/). The CloudFormation resources below are what both compile down to. ## CloudFormation Three resources: the function, an immutable revision (code + config), and an HTTPS endpoint weighted to that revision. ```yaml Resources: WebFunction: # just the name Type: AWS::Lambda::WebFunction Properties: FunctionName: {name} WebFunctionRevision: # immutable code + config Type: AWS::Lambda::WebFunctionRevision DependsOn: WebFunction Properties: FunctionName: {name} BuildConfig: CodeConfig: S3Object: Bucket: {bucket} Key: "{name}/function.zip" # VersionId: {version} # optional; use it to pick up a new upload under the same key RuntimeConfig: Runtime: {runtime} ServiceConfig: ExecutionRoleArn: !GetAtt WebFunctionRole.Arn TimeoutSeconds: 30 WebFunctionEndpoint: # HTTPS endpoint, weighted to a revision Type: AWS::Lambda::WebFunctionEndpoint DependsOn: [WebFunction, WebFunctionRevision] Properties: FunctionName: {name} EndpointName: default EndpointType: HomeRegion AuthType: IamAuth # SigV4 callers only; ApplicationManaged = PUBLIC, app owns auth RevisionWeights: - RevisionId: !GetAtt WebFunctionRevision.RevisionId Weight: 100 Outputs: EndpointDomainName: Value: !GetAtt WebFunctionEndpoint.DomainName ``` `{runtime}` is the Node.js runtime named in `SKILL.md`. For an existing function, read the current value from `buildConfig.runtimeConfig.runtime` with `aws lambda-web get-web-function-revision`. You must create the execution role yourself (trust `lambda.amazonaws.com`, attach `AWSLambdaBasicExecutionRole` for CloudWatch Logs) — see [iam-and-security.md](references/iam-and-security.md). ## VPC Egress in a Stack To reach private VPC resources, add an `AWS::Lambda::NetworkConnector` and reference it from the revision's `ServiceConfig.EgressNetworkConnectorArn`. One stack can own the whole path — the connector does not have to exist first, and you never write a version number by hand. For what a connector does and how to size the subnets, security groups and operator role, see [networking.md](references/networking.md). ```yaml Connector: Type: AWS::Lambda::NetworkConnector Properties: Name: {connector-name} OperatorRole: !GetAtt ConnectorOperatorRole.Arn Configuration: VpcEgressConfiguration: SubnetIds: [{subnet-a}, {subnet-b}, {subnet-c}] SecurityGroupIds: [{connector-sg}] NetworkProtocol: IPv4 # DualStack for IPv6 egress AssociatedComputeResourceTypes: - WebFunction WebFunctionRevision: Type: AWS::Lambda::WebFunctionRevision DependsOn: WebFunction Properties: FunctionName: {name} BuildConfig: CodeConfig: S3Object: Bucket: {bucket} Key: "{name}/function.zip" RuntimeConfig: Runtime: {runtime} ServiceConfig: ExecutionRoleArn: !GetAtt WebFunctionRole.Arn TimeoutSeconds: 30 EgressNetworkConnectorArn: !GetAtt Connector.LatestVersionArn ``` **Use `LatestVersionArn`, not `Arn`.** The revision needs a version-qualified ARN (`...:network-connector:nc-...:1`). `!GetAtt Connector.Arn` is the unqualified ARN and is rejected by resource-model validation before anything is created. Creating the connector takes a few minutes, so the stack sits on that resource before the revision starts. Give the endpoint `EndpointType: HomeRegion`. A connector is Regional, so the API rejects a `MultiRegion` or `PerRegion` endpoint that would serve a revision referencing one, and the stack fails on the endpoint resource. Updating the connector's configuration publishes a new version, so `LatestVersionArn` changes, and the next `update-stack` replaces the revision with one naming the new version and shifts the endpoint to it. The CLI path does not do this: `aws lambda-web deploy` carries the previous revision's connector ARN forward verbatim, and that ARN is version-qualified, so a redeploy keeps the old version. ## CloudFront-Only Access in a Stack To let only a CloudFront distribution invoke an `IamAuth` endpoint, attach the function's resource-based policy with `AWS::Lambda::ResourcePolicy`. The property is **`ResourceArn`**, set to the web-function ARN; `FunctionResourceArn` is not a property and CloudFormation rejects it. The distribution's origin uses an `AWS::CloudFront::OriginAccessControl` with `OriginAccessControlOriginType: lambda` and `SigningBehavior: always-amz-auth`. Why each piece is needed: [domains-and-routing.md](references/domains-and-routing.md). ```yaml OriginAccessControl: Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Name: {name}-oac OriginAccessControlOriginType: lambda SigningBehavior: always-amz-auth SigningProtocol: sigv4 # On the distribution's origin: OriginAccessControlId: !GetAtt OriginAccessControl.Id CloudFrontInvokePolicy: Type: AWS::Lambda::ResourcePolicy Properties: ResourceArn: !GetAtt WebFunction.FunctionArn # the web-function/... ARN PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: { Service: cloudfront.amazonaws.com } Action: lambda:InvokeWebFunctionEndpoint Resource: !GetAtt WebFunctionEndpoint.EndpointArn Condition: StringEquals: aws:SourceAccount: !Ref AWS::AccountId aws:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${Distribution}" ``` `aws:SourceArn` must be the distribution's full ARN, so build it with `!Sub` as above. `!Ref Distribution` returns only the distribution ID and `AWS::CloudFront::Distribution` has no `Arn` attribute, so either form gives a condition that never matches and CloudFront gets 403. In CDK the generated L1 is `CfnResourcePolicy` (`aws-cdk-lib/aws-lambda`), whose property is `resourceArn`; pass `fn.attrFunctionArn` from `CfnWebFunction`. A literal web-function ARN string makes `cdk synth` warn that `ResourceArn` does not match the pattern (`CloudFormation-Validate::F3017` and `F3031`), because CDK's built-in validation carries an older schema; cfn-lint can report the same. The template is valid and the stack deploys; do not switch to a custom resource to set the policy. ## CDK There is no L2 construct for Web Functions. If your `aws-cdk-lib` has no generated L1 (`CfnWebFunction`), declare the three resources with the escape hatch, `CfnResource`, which takes the same property names as the YAML above: ```ts const fn = new cdk.CfnResource(this, 'WebFunction', { type: 'AWS::Lambda::WebFunction', properties: { FunctionName: name }, }); const rev = new cdk.CfnResource(this, 'WebFunctionRevision', { type: 'AWS::Lambda::WebFunctionRevision', properties: { FunctionName: name, // the bare name, never getAtt('FunctionArn') BuildConfig: { CodeConfig: { S3Object: { Bucket: bucket, Key: key } }, RuntimeConfig: { Runtime: runtime }, }, ServiceConfig: { ExecutionRoleArn: role.roleArn, EgressNetworkConnectorArn: connector.getAtt('LatestVersionArn').toString(), }, }, }); rev.addDependency(fn); ``` The connector is the exception: `aws-cdk-lib` does generate `CfnNetworkConnector` (`aws-cdk-lib/aws-lambda`), so use it directly rather than a second `CfnResource`. Two CDK-specific points: - **A bare name creates no reference, so ordering is yours to declare.** In YAML that is `DependsOn`; in CDK it is `addDependency`. Without it the revision is created in parallel with the function and fails with `ResourceNotFoundException`, `The function ... does not exist`. - **`cdk synth` and `cdk deploy` can print warnings that do not mean the template is wrong.** `Unknown resource type 'AWS::Lambda::WebFunction' (CloudFormation Validate)` appears when the installed `aws-cdk-lib` does not know the type; the deploy still succeeds. On the connector, `'LatestVersionArn' is not one of ['Arn', 'State']` appears because both the generated construct and the type's published attribute list carry only those two names, while `getAtt` on `LatestVersionArn` still resolves and the deploy succeeds. Neither warning is a reason to fall back to the unqualified `Arn`, which a revision rejects. ## S3 Code Bucket Upload the zip, including `node_modules`, before deploying — nothing packages the code for you. ```bash zip -r function.zip index.js package.json node_modules/ aws s3 cp function.zip s3://{bucket}/{name}/function.zip --region {region} ``` - Versioning must be enabled (required by `AWS::Lambda::WebFunctionRevision`). - The code artifact is encrypted at rest by default (SSE-S3); for a customer-managed key, see [getting-started.md](references/getting-started.md). The function's log group is not encrypted with your own KMS key by default; to associate one, see [observability.md](references/observability.md). - Bucket policy must grant Lambda read access: ```json { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": ["s3:GetObject", "s3:GetObjectVersion"], "Resource": "arn:aws:s3:::{bucket}/*", "Condition": { "StringEquals": {"aws:SourceAccount": "{account-id}"}, "ArnLike": {"aws:SourceArn": "arn:aws:lambda:{region}:{account-id}:web-function/*"} } }, { "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": ["arn:aws:s3:::{bucket}", "arn:aws:s3:::{bucket}/*"], "Condition": {"Bool": {"aws:SecureTransport": "false"}} }] } ``` `aws:SourceArn` must end in `/*`: the source ARN is under the revision, not the function itself, so the bare function ARN matches nothing and the read is refused. ## Gotchas - **Use `DependsOn`**. Without it Cfn could try to create revision or endpoint before the function exists. - **A code change needs a new S3 key or version.** Re-uploading over the same key reports "No changes to deploy". Use a new key (`function-v2.zip`) or set `S3Object.VersionId`. - **`RevisionWeights` is always required** on the endpoint, even for a single revision. - **`FunctionName` must be the bare function name on every resource — never the function ARN.** An ARN is accepted at create time and the resources come up, then the endpoint's `DomainName` attribute cannot be read: `Property '/FunctionName' has value '{arn}' in the resource model but '{name}' in the ARN`. That fails the stack, and the rollback fails the same way on the revision, leaving `ROLLBACK_FAILED`. Recovery takes two calls, because retention is rejected until a delete has already failed: `delete-stack` to reach `DELETE_FAILED`, then `delete-stack --retain-resources WebFunctionRevision`. `!GetAtt WebFunctionRevision.RevisionId` on the endpoint is fine — the restriction is on `FunctionName`. - **Do not output the connector's `LatestVersion`.** It is an integer, and an Output value must be a string: `!GetAtt` fails the stack with "Every Value member must be a string", and `!Sub '${Connector.LatestVersion}'` fails with "does not resolve to a string". Output `LatestVersionArn` instead, which is the value a revision needs anyway. ## Verify a Stack Deploy ```bash # Get the endpoint domain from stack outputs, then curl it ENDPOINT=$(aws cloudformation describe-stacks \ --stack-name {stack-name} --region {region} \ --query "Stacks[0].Outputs[?OutputKey=='EndpointDomainName'].OutputValue" --output text) curl https://$ENDPOINT/ ``` ### Cleanup ```bash aws cloudformation delete-stack --stack-name {stack-name} --region {region} ```