diff --git a/.github/scripts/publish-layer-parameter.sh b/.github/scripts/publish-layer-parameter.sh new file mode 100755 index 00000000..09952dbf --- /dev/null +++ b/.github/scripts/publish-layer-parameter.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# +# Publishes the version-named SSM pointer after CloudFormation has deployed a layer. +# The parameter deliberately lives outside the stack: retaining a +# parameter whose Name contains the package version made CloudFormation collide with +# the retained resource whenever a version was re-used. +# +# The PipelineExecutionRole's ssm:PutParameter grant for these names lives in +# LambdaWebAdapterInfrastructureCDK (lib/infra-template.yaml, +# PipelineExecutionRolePermissions), which also denies the `latest` pointer that +# CloudFormation owns. +# +# Usage: +# publish-layer-parameter.sh +set -euo pipefail +export AWS_PAGER="" + +STACK_NAME="${1:?stack name required}" +ARCHITECTURE="${2:?architecture required}" +VERSION="${3:?version required}" +REGION="${4:?region required}" + +# A version containing a slash would write outside the path the grant covers — and, worse, +# could name a `latest` pointer. Fail before the API call rather than on an opaque +# AccessDenied. +if [[ "$VERSION" != "${VERSION//\//}" || -z "$VERSION" ]]; then + echo "Refusing to publish a parameter for version '$VERSION'" >&2 + exit 2 +fi + +case "$ARCHITECTURE" in + x86_64) + OUTPUT_KEY="LambdaAdapterLayerX86Arn" + DESCRIPTION_ARCH="X86_64" + ;; + arm64) + OUTPUT_KEY="LambdaAdapterLayerArm64Arn" + DESCRIPTION_ARCH="Arm64" + ;; + *) + echo "Unsupported architecture: $ARCHITECTURE" >&2 + exit 2 + ;; +esac + +LAYER_ARN="$( + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --region "$REGION" \ + --query "Stacks[0].Outputs[?OutputKey=='${OUTPUT_KEY}'].OutputValue | [0]" \ + --output text +)" + +if [[ -z "$LAYER_ARN" || "$LAYER_ARN" == "None" ]]; then + echo "Stack $STACK_NAME has no $OUTPUT_KEY output in $REGION" >&2 + exit 1 +fi + +PARAMETER_NAME="/lambda-web-adapter/layer/${ARCHITECTURE}/${VERSION}" +PARAMETER_VERSION="$( + aws ssm put-parameter \ + --name "$PARAMETER_NAME" \ + --description "Layer ARN for the Lambda Web Adapter ${DESCRIPTION_ARCH} Layer: ${VERSION}" \ + --type String \ + --value "$LAYER_ARN" \ + --overwrite \ + --region "$REGION" \ + --query Version \ + --output text +)" + +echo "Published $PARAMETER_NAME -> $LAYER_ARN (parameter version $PARAMETER_VERSION)" diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 0bfda68b..401202e2 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -228,13 +228,19 @@ jobs: run: | echo "CARGO_PKG_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[0].version')" >> $GITHUB_ENV + - name: Resolve the layer stack names + run: | + # Once, from BETA_STACK_NAME, so the deploy and the publish below cannot drift. + echo "X86_STACK=${BETA_STACK_NAME}-x86" >> $GITHUB_ENV + echo "ARM64_STACK=${BETA_STACK_NAME}-arm64" >> $GITHUB_ENV + - uses: actions/download-artifact@v4 with: name: packaged-beta-x86_64.yaml - name: Deploy x86_64 layer to beta account run: | - sam deploy --stack-name ${BETA_STACK_NAME}-x86 \ + sam deploy --stack-name ${X86_STACK} \ --template packaged-beta-x86_64.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -250,7 +256,7 @@ jobs: - name: Deploy arm64 layer to beta account run: | - sam deploy --stack-name ${BETA_STACK_NAME}-arm64 \ + sam deploy --stack-name ${ARM64_STACK} \ --template packaged-beta-arm64.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -260,6 +266,23 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${BETA_CLOUDFORMATION_EXECUTION_ROLE} + # Both deploys complete before either pointer is written, deliberately. These steps + # need ssm:PutParameter, granted to the PipelineExecutionRole by + # LambdaWebAdapterInfrastructureCDK (lib/infra-template.yaml, + # PipelineExecutionRolePermissions) — so a region whose pipeline-resources stack has + # not been updated with that grant fails here with AccessDenied. Ordered this way, + # that failure leaves both layers published and the version pointers stale; ordered + # between the deploys, it left the region with a new x86 layer and no arm64 one. + - name: Publish versioned x86_64 layer parameter + run: | + .github/scripts/publish-layer-parameter.sh \ + "$X86_STACK" x86_64 "${CARGO_PKG_VERSION}" "${BETA_REGION}" + + - name: Publish versioned arm64 layer parameter + run: | + .github/scripts/publish-layer-parameter.sh \ + "$ARM64_STACK" arm64 "${CARGO_PKG_VERSION}" "${BETA_REGION}" + e2e-test-zip: needs: [deploy-beta] runs-on: ubuntu-24.04 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8ecef22a..cec6bbf9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -428,6 +428,11 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: ${{fromJSON(needs.load-matrices.outputs.gamma)}} + env: + # One definition each, used by the deploy and by the publish step + # below, so the two cannot drift apart in a copy-paste. + X86_STACK: lambda-adapter-gamma-x86-${{ matrix.region }} + ARM64_STACK: lambda-adapter-gamma-arm64-${{ matrix.region }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 @@ -464,7 +469,7 @@ jobs: - name: Deploy x86_64 Layer to all regions in gamma account run: | - sam deploy --stack-name lambda-adapter-gamma-x86-${{ matrix.region }} \ + sam deploy --stack-name ${X86_STACK} \ --template packaged-gamma-x86_64-${{ matrix.region }}.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -474,6 +479,7 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${{ matrix.cloudformation_execution_role }} + - uses: actions/download-artifact@v4 with: name: packaged-gamma-arm64-${{ matrix.region }}.yaml @@ -481,7 +487,7 @@ jobs: - name: Deploy arm64 Layer to supported regions in gamma account if: ${{ matrix.arm64_supported }} run: | - sam deploy --stack-name lambda-adapter-gamma-arm64-${{ matrix.region }} \ + sam deploy --stack-name ${ARM64_STACK} \ --template packaged-gamma-arm64-${{ matrix.region }}.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -491,12 +497,37 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${{ matrix.cloudformation_execution_role }} + # Both deploys complete before either pointer is written, deliberately. These steps + # need ssm:PutParameter, granted to the PipelineExecutionRole by + # LambdaWebAdapterInfrastructureCDK (lib/infra-template.yaml, + # PipelineExecutionRolePermissions) — so a region whose pipeline-resources stack has + # not been updated with that grant fails here with AccessDenied. Ordered this way, + # that failure leaves both layers published and the version pointers stale; ordered + # between the deploys, it left the region with a new x86 layer and no arm64 one. + - name: Publish versioned x86_64 layer parameter + run: | + .github/scripts/publish-layer-parameter.sh \ + "$X86_STACK" \ + x86_64 "${CARGO_PKG_VERSION}" "${{ matrix.region }}" + + - name: Publish versioned arm64 layer parameter + if: ${{ matrix.arm64_supported }} + run: | + .github/scripts/publish-layer-parameter.sh \ + "$ARM64_STACK" \ + arm64 "${CARGO_PKG_VERSION}" "${{ matrix.region }}" + deploy-prod: needs: [load-matrices, deploy-gamma, package-prod] runs-on: ubuntu-24.04 environment: prod strategy: matrix: ${{fromJSON(needs.load-matrices.outputs.prod)}} + env: + # One definition each, used by the deploy and by the publish step + # below, so the two cannot drift apart in a copy-paste. + X86_STACK: lambda-adapter-prod-x86-${{ matrix.region }} + ARM64_STACK: lambda-adapter-prod-arm64-${{ matrix.region }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 @@ -533,7 +564,7 @@ jobs: - name: Deploy x86_64 Layer to all regions in prod account run: | - sam deploy --stack-name lambda-adapter-prod-x86-${{ matrix.region }} \ + sam deploy --stack-name ${X86_STACK} \ --template packaged-prod-x86_64-${{ matrix.region }}.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -543,6 +574,7 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${{ matrix.cloudformation_execution_role }} + - uses: actions/download-artifact@v4 with: name: packaged-prod-arm64-${{ matrix.region }}.yaml @@ -550,7 +582,7 @@ jobs: - name: Deploy arm64 Layer to supported regions in prod account if: ${{ matrix.arm64_supported }} run: | - sam deploy --stack-name lambda-adapter-prod-arm64-${{ matrix.region }} \ + sam deploy --stack-name ${ARM64_STACK} \ --template packaged-prod-arm64-${{ matrix.region }}.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -560,11 +592,29 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${{ matrix.cloudformation_execution_role }} + - name: Publish versioned x86_64 layer parameter + run: | + .github/scripts/publish-layer-parameter.sh \ + "$X86_STACK" \ + x86_64 "${CARGO_PKG_VERSION}" "${{ matrix.region }}" + + - name: Publish versioned arm64 layer parameter + if: ${{ matrix.arm64_supported }} + run: | + .github/scripts/publish-layer-parameter.sh \ + "$ARM64_STACK" \ + arm64 "${CARGO_PKG_VERSION}" "${{ matrix.region }}" + deploy-china-gamma: needs: [load-matrices, package-china-gamma] runs-on: ubuntu-24.04 strategy: matrix: ${{fromJSON(needs.load-matrices.outputs.china-gamma)}} + env: + # One definition each, used by the deploy and by the publish step + # below, so the two cannot drift apart in a copy-paste. + X86_STACK: lambda-adapter-gamma-x86-${{ matrix.region }} + ARM64_STACK: lambda-adapter-gamma-arm64-${{ matrix.region }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 @@ -603,7 +653,7 @@ jobs: - name: Deploy x86_64 Layer to all regions in china run: | - sam deploy --stack-name lambda-adapter-gamma-x86-${{ matrix.region }} \ + sam deploy --stack-name ${X86_STACK} \ --template packaged-china-gamma-x86_64-${{ matrix.region }}.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -613,6 +663,7 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${{ matrix.cloudformation_execution_role }} + - uses: actions/download-artifact@v4 with: name: packaged-china-gamma-arm64-${{ matrix.region }}.yaml @@ -620,7 +671,7 @@ jobs: - name: Deploy arm64 Layer to supported china regions if: ${{ matrix.arm64_supported }} run: | - sam deploy --stack-name lambda-adapter-gamma-arm64-${{ matrix.region }} \ + sam deploy --stack-name ${ARM64_STACK} \ --template packaged-china-gamma-arm64-${{ matrix.region }}.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -630,12 +681,30 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${{ matrix.cloudformation_execution_role }} + - name: Publish versioned x86_64 layer parameter + run: | + .github/scripts/publish-layer-parameter.sh \ + "$X86_STACK" \ + x86_64 "${CARGO_PKG_VERSION}" "${{ matrix.region }}" + + - name: Publish versioned arm64 layer parameter + if: ${{ matrix.arm64_supported }} + run: | + .github/scripts/publish-layer-parameter.sh \ + "$ARM64_STACK" \ + arm64 "${CARGO_PKG_VERSION}" "${{ matrix.region }}" + deploy-china-prod: needs: [load-matrices, deploy-china-gamma, package-china-prod] runs-on: ubuntu-24.04 environment: prod strategy: matrix: ${{fromJSON(needs.load-matrices.outputs.china-prod)}} + env: + # One definition each, used by the deploy and by the publish step + # below, so the two cannot drift apart in a copy-paste. + X86_STACK: lambda-adapter-prod-x86-${{ matrix.region }} + ARM64_STACK: lambda-adapter-prod-arm64-${{ matrix.region }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 @@ -674,7 +743,7 @@ jobs: - name: Deploy x86_64 Layer to all regions in china run: | - sam deploy --stack-name lambda-adapter-prod-x86-${{ matrix.region }} \ + sam deploy --stack-name ${X86_STACK} \ --template packaged-china-prod-x86_64-${{ matrix.region }}.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -684,6 +753,7 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${{ matrix.cloudformation_execution_role }} + - uses: actions/download-artifact@v4 with: name: packaged-china-prod-arm64-${{ matrix.region }}.yaml @@ -691,7 +761,7 @@ jobs: - name: Deploy arm64 Layer to supported china regions if: ${{ matrix.arm64_supported }} run: | - sam deploy --stack-name lambda-adapter-prod-arm64-${{ matrix.region }} \ + sam deploy --stack-name ${ARM64_STACK} \ --template packaged-china-prod-arm64-${{ matrix.region }}.yaml \ --parameter-overrides CargoPkgVersion=${CARGO_PKG_VERSION} \ --capabilities CAPABILITY_IAM \ @@ -701,6 +771,19 @@ jobs: --no-fail-on-empty-changeset \ --role-arn ${{ matrix.cloudformation_execution_role }} + - name: Publish versioned x86_64 layer parameter + run: | + .github/scripts/publish-layer-parameter.sh \ + "$X86_STACK" \ + x86_64 "${CARGO_PKG_VERSION}" "${{ matrix.region }}" + + - name: Publish versioned arm64 layer parameter + if: ${{ matrix.arm64_supported }} + run: | + .github/scripts/publish-layer-parameter.sh \ + "$ARM64_STACK" \ + arm64 "${CARGO_PKG_VERSION}" "${{ matrix.region }}" + publish-to-public-ecr: needs: [deploy-prod] runs-on: ubuntu-24.04 diff --git a/template-arm64.yaml b/template-arm64.yaml index 5f73ab07..b0c4830c 100644 --- a/template-arm64.yaml +++ b/template-arm64.yaml @@ -39,10 +39,9 @@ Resources: Type: String Value: !Ref LambdaAdapterLayerArm64 - # A per-version parameter (/lambda-web-adapter/layer/arm64/) used to - # live here. See the matching comment in template-x86_64.yaml for why it was removed: - # a Name change replaces the parameter, and UpdateReplacePolicy: Retain orphaned the - # old name, so re-deploying an already-used version failed with AlreadyExists. + # The version-named parameter (/lambda-web-adapter/layer/arm64/) + # is published after deployment with `aws ssm put-parameter --overwrite`. See the + # matching comment in template-x86_64.yaml for why it cannot live in this stack. Outputs: LambdaAdapterLayerArm64Arn: diff --git a/template-x86_64.yaml b/template-x86_64.yaml index d857dbfc..a8455cd2 100644 --- a/template-x86_64.yaml +++ b/template-x86_64.yaml @@ -39,14 +39,10 @@ Resources: Type: String Value: !Ref LambdaAdapterLayerX86 - # A per-version parameter (/lambda-web-adapter/layer/x86_64/) used - # to live here. It was removed: because CloudFormation replaces a parameter when its - # Name changes, and UpdateReplacePolicy was Retain, every version bump orphaned the - # previous name. Deploying a version whose name had already been orphaned — after the - # v1.1.0 revert, or on any re-release — then failed with AlreadyExists and wedged - # every merge to main. Existing per-version parameters are retained, and the layer's - # Description still records the version, so `aws lambda list-layer-versions` maps a - # version to its layer ARN. + # The version-named parameter (/lambda-web-adapter/layer/x86_64/) + # is published after deployment with `aws ssm put-parameter --overwrite`. It cannot + # live in this stack: a Name change replaces the resource, while retaining old + # versions leaves names that CloudFormation collides with when a version is re-used. Outputs: LambdaAdapterLayerX86Arn: