Skip to content

Commit 2b000fd

Browse files
feat(jobs): move snapshots to s3 (#113)
* feat(jobs): move snapshots to s3 * feat(jobs): s3 snapshot utility * modernize project structure * update readme
1 parent c1dbcf6 commit 2b000fd

7 files changed

Lines changed: 389 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ Table of Contents:
9494
| **[Instance Snapshot Cleaner](jobs/instances-snapshot-cleaner/README.md)** <br/> Use Serverless Jobs to clean old instances snapshots | Go | [Console] |
9595
| **[Registry Tag Cleaner](jobs/registry-version-based-retention/README.md)** <br/> Use Serverless Jobs to keep a desired amount of tags for each image | Go | [Console] |
9696
| **[Registry Empty Image Cleaner](jobs/registry-empty-ressource-cleaner/README.md)** <br/> Use Serverless Jobs to clean container registry empty namespaces and images | Go | [Console] |
97+
| **[Instance snapshots to S3](jobs/block-snapshot-s3-archiver/README.md)** <br/> Use Serverless Jobs to move Instances snapshots to S3 Object Storage | Go | [Console] |
9798
| **[Block snapshots to S3](jobs/block-snapshot-s3-archiver/README.md)** <br/> Use Serverless Jobs to move Block Storage snapshots to S3 Object Storage | Go | [Console] |
9899

99100
### 💬 Messaging and Queueing
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Build stage
2+
FROM golang:1.25-alpine AS builder
3+
4+
WORKDIR /app
5+
6+
# Copy required files
7+
COPY go.mod go.sum ./
8+
RUN go mod download
9+
10+
COPY *.go ./
11+
12+
# Build the executable
13+
RUN CGO_ENABLED=0 GOOS=linux go build -o /jobs-snapshot-s3
14+
15+
# Final stage
16+
FROM alpine:latest
17+
18+
WORKDIR /app
19+
20+
# Install CA certificates for HTTPS
21+
RUN apk --no-cache add ca-certificates
22+
23+
# Copy the binary from the builder stage
24+
COPY --from=builder /jobs-snapshot-s3 /app/jobs-snapshot-s3
25+
26+
# Run the executable
27+
ENTRYPOINT [ "/app/jobs-snapshot-s3" ]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Scaleway Instance Snapshot Archiver
2+
3+
Automated Serverless Job to archive Scaleway Instance snapshots to Object Storage S3.
4+
5+
## Overview
6+
7+
This tool automatically finds available snapshots of Scaleway Instances volumes, exports them to a specified S3 bucket in `.qcow2` format, and deletes the original snapshot to optimize storage costs. It's designed to run as a Serverless Job on Scaleway and skips snapshots that have already been archived.
8+
9+
The main logic is implemented in `main.go`, which:
10+
1. Loads configuration from environment variables.
11+
2. Connects to Scaleway APIs using the Scaleway SDK.
12+
3. Lists all available snapshots in the project.
13+
4. Checks the target S3 bucket for already-archived snapshots.
14+
5. Exports new snapshots to the bucket.
15+
6. Deletes successfully exported snapshots to reduce storage costs.
16+
17+
## Features
18+
19+
- **Automated Export**: Finds available snapshots and exports them to an S3 bucket in `.qcow2` format.
20+
- **Cost Optimization**: Deletes the source snapshot after successful export to reduce storage costs.
21+
- **Idempotent**: Skips snapshots that are already archived in the bucket.
22+
- **Serverless Ready**: Designed for [Scaleway Serverless Jobs](https://www.scaleway.com/en/serverless-jobs/).
23+
24+
## Step 1 : Build and push to Container registry
25+
26+
Serverless Jobs, like Serverless Containers (which are suited for HTTP applications), works
27+
with containers. So first, use your terminal reach this folder and run the following commands:
28+
29+
```shell
30+
# First command is to login to container registry, you can find it in Scaleway console
31+
docker login rg.fr-par.scw.cloud/snapshot-s3-archiver -u nologin --password-stdin <<< "$SCW_SECRET_KEY"
32+
33+
# Here we build the image to push
34+
docker buildx build --platform linux/amd64 -t rg.fr-par.scw.cloud/snapshot-s3-archiver/snapshot-s3-archiver:v1 .
35+
36+
# Push the image online to be used on Serverless Jobs
37+
docker push rg.fr-par.scw.cloud/snapshot-s3-archiver/snapshot-s3-archiver:v1
38+
```
39+
> [!TIP]
40+
> As we do not expose a web server and we do not require features such as auto-scaling, Serverless Jobs are perfect for this use case.
41+
To check if everyting is ok, on the Scaleway Console you can verify if your tag is present in Container Registry.
42+
43+
## Step 2: Creating the Job Definition
44+
45+
On Scaleway Console on the following link you can create a new Job Definition: https://console.scaleway.com/serverless-jobs/jobs/create?region=fr-par
46+
47+
1. On Container image, select the image you created in the step before.
48+
2. You can set the job definition name name to something clear.
49+
3. Regarding the resources you can keep the default values, this job is fast and do not require specific compute power or memory.
50+
4. To schedule your job for example every night at 2am, you can set the cron to `0 2 * * *`.
51+
5. Important: advanced option, you need to set the following environment variables:
52+
53+
> [!TIP]
54+
> For sensitive data like `SCW_ACCESS_KEY` and `SCW_SECRET_KEY` we recommend to inject them via Secret Manager, [more info here](https://www.scaleway.com/en/docs/serverless/jobs/how-to/reference-secret-in-job/).
55+
| Variable | Description |
56+
|---|---|
57+
| `SCW_DEFAULT_ORGANIZATION_ID` | Organization ID . |
58+
| `SCW_DEFAULT_PROJECT_ID` | Project ID (Recommended resource grouping). |
59+
| `SCW_ACCESS_KEY` | IAM Access Key. |
60+
| `SCW_SECRET_KEY` | IAM Secret Key. |
61+
| `SCW_ZONE` | Zone of the snapshots (e.g., `fr-par-1`). |
62+
| `SCW_BUCKET_NAME` | S3 Bucket name for archives. |
63+
| `SCW_BUCKET_ENDPOINT` | S3 Endpoint (e.g., `s3.fr-par.scw.cloud`). |
64+
65+
* Then click "create job"
66+
67+
## Step 3: Run the job
68+
69+
On your created Job Definition, just click the button "Run Job" and within seconds it should be successful.
70+
71+
## Troubleshooting
72+
73+
If your Job Run state goes in error, you can use the "Logs" tab in Scaleway Console to get more informations about the error.
74+
75+
# Additional content
76+
77+
- [Jobs Documentation](https://www.scaleway.com/en/docs/serverless/jobs/how-to/create-job-from-scaleway-registry/)
78+
- [Other methods to deploy Jobs](https://www.scaleway.com/en/docs/serverless/jobs/reference-content/deploy-job/)
79+
- [Secret key / access key doc](https://www.scaleway.com/en/docs/identity-and-access-management/iam/how-to/create-api-keys/)
80+
- [CRON schedule help](https://www.scaleway.com/en/docs/serverless/jobs/reference-content/cron-schedules/)
81+
-
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/scaleway/scaleway-sdk-go/scw"
8+
)
9+
10+
// Environment variable constants
11+
const (
12+
envOrgID = "SCW_DEFAULT_ORGANIZATION_ID"
13+
envAccessKey = "SCW_ACCESS_KEY"
14+
envSecretKey = "SCW_SECRET_KEY"
15+
envProjectID = "SCW_DEFAULT_PROJECT_ID"
16+
envZone = "SCW_ZONE"
17+
envBucket = "SCW_BUCKET_NAME"
18+
envBucketEndpoint = "SCW_BUCKET_ENDPOINT"
19+
)
20+
21+
type Config struct {
22+
OrgID string
23+
AccessKey string
24+
SecretKey string
25+
ProjectID string
26+
Zone scw.Zone
27+
BucketName string
28+
BucketEndpoint string
29+
}
30+
31+
func LoadConfig() (*Config, error) {
32+
// Mandatory variables
33+
vars := map[string]*string{
34+
envAccessKey: new(string),
35+
envSecretKey: new(string),
36+
envProjectID: new(string),
37+
envZone: new(string),
38+
envBucket: new(string),
39+
envBucketEndpoint: new(string),
40+
}
41+
42+
// Optional variables
43+
orgID := os.Getenv(envOrgID)
44+
45+
for envKey, valPtr := range vars {
46+
val := os.Getenv(envKey)
47+
if val == "" {
48+
return nil, fmt.Errorf("missing environment variable %s", envKey)
49+
}
50+
*valPtr = val
51+
}
52+
53+
return &Config{
54+
OrgID: orgID,
55+
AccessKey: *vars[envAccessKey],
56+
SecretKey: *vars[envSecretKey],
57+
ProjectID: *vars[envProjectID],
58+
Zone: scw.Zone(*vars[envZone]),
59+
BucketName: *vars[envBucket],
60+
BucketEndpoint: *vars[envBucketEndpoint],
61+
}, nil
62+
}

jobs/snapshot-s3-archiver/go.mod

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module github.com/scaleway/serverless-examples/jobs/snapshot-s3-archiver
2+
3+
go 1.25
4+
5+
require (
6+
github.com/minio/minio-go/v7 v7.0.95
7+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35
8+
)
9+
10+
require (
11+
github.com/dustin/go-humanize v1.0.1 // indirect
12+
github.com/go-ini/ini v1.67.0 // indirect
13+
github.com/goccy/go-json v0.10.5 // indirect
14+
github.com/google/uuid v1.6.0 // indirect
15+
github.com/klauspost/compress v1.18.0 // indirect
16+
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
17+
github.com/kr/pretty v0.3.1 // indirect
18+
github.com/minio/crc64nvme v1.1.1 // indirect
19+
github.com/minio/md5-simd v1.1.2 // indirect
20+
github.com/philhofer/fwd v1.2.0 // indirect
21+
github.com/rogpeppe/go-internal v1.14.1 // indirect
22+
github.com/rs/xid v1.6.0 // indirect
23+
github.com/stretchr/testify v1.11.1 // indirect
24+
github.com/tinylib/msgp v1.4.0 // indirect
25+
golang.org/x/crypto v0.42.0 // indirect
26+
golang.org/x/net v0.44.0 // indirect
27+
golang.org/x/sys v0.36.0 // indirect
28+
golang.org/x/text v0.29.0 // indirect
29+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
30+
gopkg.in/yaml.v2 v2.4.0 // indirect
31+
)

jobs/snapshot-s3-archiver/go.sum

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
5+
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
6+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
7+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
8+
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
9+
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
10+
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
11+
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
12+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
13+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
14+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
15+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
16+
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
17+
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
18+
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
19+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
20+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
21+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
22+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
23+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
24+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
25+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
26+
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
27+
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
28+
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
29+
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
30+
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
31+
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
32+
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
33+
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
34+
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
35+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
36+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
37+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
38+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
39+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
40+
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
41+
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
42+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
43+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
44+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
45+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
46+
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
47+
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
48+
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
49+
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
50+
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
51+
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
52+
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
53+
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
54+
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
55+
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
56+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
57+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
58+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
59+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
60+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
61+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
62+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)