Automatically redeploys a Yandex Cloud Serverless Container whenever a new image tag is pushed to Container Registry.
Container Registry push
│
▼
YC Function Trigger
(one per image repo)
│
▼
Cloud Function (Go)
│
1. Resolve container ID from IMAGE_CONTAINER_MAP
2. Fetch active revision config
3. Deploy new revision with updated imageUrl
│
▼
Serverless Container
running the new image
.
├── function/ # Go source for the Cloud Function
│ ├── main.go # Handler entrypoint + helper functions
│ ├── go.mod
│ └── go.sum
└── terraform/ # Infrastructure as code
├── main.tf # Providers, archive data source
├── function.tf # yandex_function resource
├── trigger.tf # yandex_function_trigger (one per image)
├── iam.tf # Service accounts and IAM bindings
├── variables.tf # Input variable definitions
├── outputs.tf # Output values
└── terraform.tfvars.example
- Yandex Cloud CLI (
yc) - Terraform >= 1.3
- Go 1.23+ (for local builds / tests only)
- A Yandex Cloud folder with billing enabled
- An existing Container Registry
cd terraform
cp terraform.tfvars.example terraform.tfvarsEdit terraform.tfvars:
| Variable | Description |
|---|---|
folder_id |
Yandex Cloud folder ID (yc resource-manager folder list) |
registry_id |
Container Registry ID (yc container registry list) |
image_container_map |
Deploy channels: label → {image, container_id, registry_id?, tag?} |
function_name |
Cloud Function name (default: registry-deploy) |
function_memory |
Memory in MB (default: 128) |
function_timeout |
Timeout in seconds (default: 30) |
Example image_container_map:
image_container_map = {
# Simplest case: default registry, any tag, one container.
myapp = {
image = "myapp"
container_id = "bba..."
}
# Another registry, and one image feeding two containers by tag.
otherapp-test = {
image = "otherapp"
registry_id = "crp..."
tag = "test"
container_id = "bbb..."
secret_ids = ["e6q..."]
}
otherapp-prod = {
image = "otherapp"
registry_id = "crp..."
tag = "prod"
container_id = "bbc..."
secret_ids = ["e6q..."]
}
}The key is a label for the channel, not the image name — that is what lets
otherapp-testandotherapp-prodwatch one image and reach two containers.registry_idis per entry and defaults to the top-levelregistry_id, so one stack can serve several registries.tagnarrows both the trigger and the routing; omit it and any tag on that image fires the channel.
secret_idslists the Lockbox secrets that container's revision mounts. A container with secrets cannot be redeployed without it: the function copies the revision'ssecretsblock verbatim, and Yandex Cloud refuses to attach a secret the caller cannot read, soDeployRevisionanswers403 Permission denied— after the event has been parsed and the container resolved, which makes the trigger look healthy while nothing deploys. Find them with:yc serverless container revision get <revision-id> --format json | jq '.secrets'Terraform builds the
IMAGE_CONTAINER_MAPenv var from these entries, prefixing the registry so keys match therepository_namefield in trigger events (crp.../myapp), and appending:tagwhere a channel pins one.
export YC_SERVICE_ACCOUNT_KEY_FILE=$(cat key.json)
terraform init
terraform plan
terraform applyTerraform will create:
- One Cloud Function (
registry-deploy) with the Go handler zipped and uploaded - One trigger per entry in
image_container_map, each scoped to its repository name - Two service accounts with minimal IAM roles:
- function-sa —
serverless-containers.editor,iam.serviceAccounts.user,vpc.user - trigger-sa —
serverless.functions.invoker(invokes the function)
- function-sa —
Add an entry to image_container_map in terraform.tfvars and re-run terraform apply. A new trigger is created automatically; no code changes needed.
Two channels on one image are a promotion path: CI pushes the test tag on
merge, and production is deployed by moving prod onto a build that has
already proven itself in test. Moving the tag is the whole deploy — no call to
the Serverless Containers API, so the promoting workflow needs registry
credentials only:
IMAGE=cr.yandex/<registry-id>/otherapp
docker buildx imagetools create --tag "$IMAGE:prod" "$IMAGE:pr-42"Rolling back is the same command with an older source tag.
| Variable | Format | Description |
|---|---|---|
IMAGE_CONTAINER_MAP |
JSON {"registry_id/repo[:tag]": "container-id"} |
Auto-set by Terraform from image_container_map variable |
The function prefers a key that pins the tag (repo:tag) over one that matches
the repository alone (repo), so a bare repo key still matches any tag and
maps written before tag routing keep working unchanged.
The service account used to run terraform apply (e.g. registry-deploy-sa) needs:
| Role | Purpose |
|---|---|
container-registry.images.puller |
Pull images from Container Registry |
functions.editor |
Create and manage Cloud Functions |
iam.serviceAccounts.admin |
Create and bind runtime service accounts |
resource-manager.admin |
Manage folder-level resources |
serverless-containers.editor |
Create and manage Serverless Containers |
serverless.functions.invoker |
Invoke Cloud Functions |
| Role | Assigned to | Purpose |
|---|---|---|
serverless-containers.editor |
<function_name>-sa |
Deploy new container revisions at runtime |
iam.serviceAccounts.user |
<function_name>-sa |
Assign the container's service account when deploying a revision |
vpc.user |
<function_name>-sa |
Attach a VPC network when the revision config carries connectivity settings |
serverless.functions.invoker |
<function_name>-trigger-sa |
Invoke the Cloud Function from the registry trigger |
lockbox.payloadViewer |
<function_name>-sa, per secret in secret_ids |
Attach the revision's Lockbox secrets to the new revision |
cd function
go vet ./...The package has no
main()— Yandex Cloud Functions useHandleras the entrypoint (main.Handler). That is also whygo build ./...fails here with "function main is undeclared in the main package";go vettype-checks the package and is the build check to run.
Uncomment the backend "s3" block in terraform/main.tf and set your Object Storage bucket name to store state remotely.