Tutorial: Restore a cluster from cloud storage
Learn how to back up data in Hazelcast maps to cloud storage and restore a cluster from that backup data.
Overview
In this tutorial, you'll do the following:
- Deploy Hazelcast with persistence enabled.
- Create a Hazelcast map that has persistence enabled.
- Back up all map entries to external storage in the cloud.
- Restart the Hazelcast cluster and restore the backup map entries from the cloud.
The tutorial should take approximately 20 minutes to complete.
Prerequisites
Before you begin, make sure that you have:
-
An up and running Kubernetes cluster.
-
Kubernetes command line tool, kubectl.
-
Deployed Hazelcast Platform Operator.
-
Created blob storage and access credentials in one of the Cloud providers:
-
The Hazelcast Code Samples repository https://github.com/hazelcast/hazelcast-code-samples cloned to your local machine.
Start the Hazelcast cluster
-
Create a secret with your Hazelcast Enterprise License.
shellkubectl create secret generic hazelcast-license-key --from-literal=license-key=<hz-license-key> -
Create a Hazelcast cluster with Persistence enabled:
shellkubectl apply -f - <<EOFapiVersion: hazelcast.com/v1alpha1kind: Hazelcastmetadata:name: my-hazelcastspec:clusterSize: 3licenseKeySecretName: hazelcast-license-keypersistence:pvc:accessModes: ["ReadWriteOnce"]requestStorage: 8GiexposeExternally:type: UnisocketdiscoveryServiceType: LoadBalancerEOF -
Check the cluster status:
shellkubectl get hazelcast my-hazelcastNAME STATUS MEMBERSmy-hazelcast Running 3/3shellkubectl get pods -l app.kubernetes.io/instance=my-hazelcastNAME READY STATUS RESTARTS AGEmy-hazelcast-0 2/2 Running 0 3m43smy-hazelcast-1 2/2 Running 0 3m16smy-hazelcast-2 2/2 Running 0 2m50sAs you can see from the pod states, the agent container will be deployed with the Hazelcast container in the same Pod. The agent is responsible for backing data up into the external storage.
-
After verifying that the cluster is
Runningand all the members are ready, find the discovery address:shellkubectl get hazelcastendpoint my-hazelcastNAME TYPE ADDRESSmy-hazelcast Discovery 198.51.100.128:5701The
ADDRESScolumn displays the external address of your Hazelcast cluster.
Create persistent map and put data
-
Create a Map resource with persistence enabled:
shellkubectl apply -f - <<EOFapiVersion: hazelcast.com/v1alpha1kind: Mapmetadata:name: persistent-mapspec:hazelcastResourceName: my-hazelcastpersistenceEnabled: trueEOF -
Check the status of the map configuration:
bashkubectl get map persistent-mapThe status of the map is displayed in the output.
bashNAME STATUSpersistent-map Success -
Configure the Hazelcast client to connect to the cluster.
Open the examples folder to access all sample clients:
shellcd hazelcast-platform-operator-external-backup-restoreThe sample code for this tutorial is in the
docs/modules/ROOT/examples/operator-external-backupdirectory.- CLC
- Java
- NodeJS
- Go
- Python
- .NET
noteCLC must be installed in your system. See the CLC installation instructions.
Add the cluster configuration to CLC:
bashclc config add hz cluster.name=dev cluster.address=<EXTERNAL-IP>javapackage com.hazelcast;import com.hazelcast.client.HazelcastClient;import com.hazelcast.client.config.ClientConfig;import com.hazelcast.client.config.ClientNetworkConfig;import com.hazelcast.client.impl.connection.tcp.RoutingMode;import com.hazelcast.core.HazelcastInstance;import com.hazelcast.map.IMap;import java.util.Random;public class Main {public static void main(String[] args) throws Exception {if(args.length == 0) {System.out.println("You should pass an argument to run: fill or size");} else if (!((args[0].equals("fill") || args[0].equals("size")))) {System.out.println("Wrong argument, you should pass: fill or size");} else{ClientConfig clientConfig = new ClientConfig();ClientNetworkConfig networkConfig = clientConfig.getNetworkConfig();networkConfig.addAddress("<EXTERNAL-IP>");networkConfig.getClusterRoutingConfig().setRoutingMode(RoutingMode.SINGLE_MEMBER);HazelcastInstance client = HazelcastClient.newHazelcastClient(clientConfig);System.out.println("Successful connection!");IMap<String, String> map = client.getMap("persistent-map");if (args[0].equals("fill")) {System.out.println("Starting to fill the map with random entries.");Random random = new Random();while (true) {int randomKey = random.nextInt(100_000);map.put("key-" + randomKey, "value-" + randomKey);System.out.println("Current map size: " + map.size());}} else {System.out.println("Current map size: " + map.size());client.shutdown();}}}}javascript'use strict';const { Client } = require('hazelcast-client');const clientConfig = {network: {clusterMembers: ['<EXTERNAL-IP>'],smartRouting: false}};(async () => {try {if (process.argv.length === 2) {console.error('You should pass an argument to run: fill or size');} else if (!(process.argv[2] === 'fill' || process.argv[2] === 'size')) {console.error('Wrong argument, you should pass: fill or size');} else {const client = await Client.newHazelcastClient(clientConfig);const map = await client.getMap('persistent-map');await map.put('key', 'value');const res = await map.get('key');if (res !== 'value') {throw new Error('Connection failed, check your configuration.');}console.log('Successful connection!');if (process.argv[2] === 'fill'){console.log('Starting to fill the map with random entries.');while (true) {const randomKey = Math.floor(Math.random() * 100000);await map.put('key' + randomKey, 'value' + randomKey);const size = await map.size();console.log(`Current map size: ${size}`);}} else {const size = await map.size();console.log(`Current map size: ${size}`);}}} catch (err) {console.error('Error occurred:', err);}})();gopackage mainimport ("context""fmt""math/rand""os""github.com/hazelcast/hazelcast-go-client")func main() {if len(os.Args) != 2 {fmt.Println("You should pass an argument to run: fill or size")return}if !(os.Args[1] == "fill" || os.Args[1] == "size") {fmt.Println("Wrong argument, you should pass: fill or size")return}config := hazelcast.Config{}cc := &config.Clustercc.Network.SetAddresses("<EXTERNAL-IP>:5701")cc.Unisocket = truectx := context.TODO()client, err := hazelcast.StartNewClientWithConfig(ctx, config)if err != nil {panic(err)}fmt.Println("Successful connection!")m, err := client.GetMap(ctx, "persistent-map")if err != nil {panic(err)}if os.Args[1] == "fill" {fmt.Println("Starting to fill the map with random entries.")for {num := rand.Intn(100_000)key := fmt.Sprintf("key-%d", num)value := fmt.Sprintf("value-%d", num)if _, err = m.Put(ctx, key, value); err != nil {fmt.Println("ERR:", err.Error())continue}mapSize, err := m.Size(ctx)if err != nil {fmt.Println("ERR:", err.Error())continue}fmt.Println("Current map size:", mapSize)}return}mapSize, err := m.Size(ctx)if err != nil {fmt.Println("ERR:", err.Error())return}fmt.Println("Current map size:", mapSize)}pythonimport loggingimport randomimport sysimport hazelcastlogging.basicConfig(level=logging.INFO)if len(sys.argv) != 2:print("You should pass an argument to run: fill or size")elif not (sys.argv[1] == "fill" or sys.argv[1] == "size"):print("Wrong argument, you should pass: fill or size")else:client = hazelcast.HazelcastClient(cluster_members=["<EXTERNAL-IP>"],smart_routing=False,)print("Successful connection!", flush=True)m = client.get_map("persistent-map").blocking()if sys.argv[1] == "fill":print("Starting to fill the map with random entries.", flush=True)while True:random_number = str(random.randrange(0, 100000))m.put("key-" + random_number, "value-" + random_number)print("Current map size:", m.size())else:print("Current map size:", m.size())csusing System;using System.Threading.Tasks;using Hazelcast;using Microsoft.Extensions.Logging;namespace Client{public class Program{static async Task Main(string[] args){if (args.Length != 1){Console.WriteLine("You should pass an argument to run: fill or size");return;}if (!(args[0] == "fill" || args[0] == "size")){Console.WriteLine("Wrong argument, you should pass: fill or size");return;}var options = new HazelcastOptionsBuilder().With(args).With((configuration, options) =>{options.LoggerFactory.Creator = () => LoggerFactory.Create(loggingBuilder =>loggingBuilder.AddConfiguration(configuration.GetSection("logging")).AddConsole());options.Networking.Addresses.Add("<EXTERNAL-IP>:5701");options.Networking.SmartRouting = false;}).Build();await using var client = await HazelcastClientFactory.StartNewClientAsync(options);Console.WriteLine("Successful connection!");Console.WriteLine("Starting to fill the map with random entries.");var map = await client.GetMapAsync<string, string>("persistent-map");var random = new Random();if (args[0] == "fill"){Console.WriteLine("Starting to fill the map with random entries.");while (true){var num = random.Next(100_000);var key = $"key-{num}";var value = $"value-{num}";await map.PutAsync(key, value);var mapSize = await map.GetSizeAsync();Console.WriteLine($"Current map size: {mapSize}");}}else{var mapSize = await map.GetSizeAsync();Console.WriteLine($"Current map size: {mapSize}");await client.DisposeAsync();}}}} -
Start the client to fill the map.
- CLC
- Java
- NodeJS
- Go
- Python
- .NET
Fill a map:
bashfor i in {1..10};doclc -c hz map set --name persistent-map key-$i value-$i;doneCheck the map size:
bashclc -c hz map size --name persistent-mapbashcd javamvn packagejava -jar target/*jar-with-dependencies*.jar fillYou should see the following output:
shellSuccessful connection!Starting to fill the map with random entries.Current map size: 2Current map size: 3Current map size: 4........bashcd nodejsnpm installnpm start fillYou should see the following output:
shellSuccessful connection!Starting to fill the map with random entries.Current map size: 2Current map size: 3Current map size: 4........bashcd gogo run main.go fillYou should see the following output:
shellSuccessful connection!Starting to fill the map with random entries.Current map size: 2Current map size: 3Current map size: 4........bashcd pythonpip install -r requirements.txtpython main.py fillYou should see the following output:
shellSuccessful connection!Starting to fill the map with random entries.Current map size: 2Current map size: 3Current map size: 4........bashcd dotnetdotnet builddotnet run fillshellSuccessful connection!Starting to fill the map with random entries.Current map size: 2Current map size: 3Current map size: 4........
Trigger an external backup
To trigger a backup, you need a bucketURI where backup data will be stored and a secret with credentials for accessing the bucket URI.
-
Run one of the following commands to create the secret according to the cloud provider you want to back up:
- AWS
- GCP
- Azure
bashkubectl create secret generic <external-bucket-secret-name> --from-literal=region=<region> \--from-literal=access-key-id=<access-key-id> \--from-literal=secret-access-key=<secret-access-key>bashkubectl create secret generic <external-bucket-secret-name> --from-file=google-credentials-path=<service_account_json_file>bashkubectl create secret generic <external-bucket-secret-name> \--from-literal=storage-account=<storage-account> \--from-literal=storage-key=<storage-key> -
Trigger a backup:
shellkubectl apply -f - <<EOFapiVersion: hazelcast.com/v1alpha1kind: HotBackupmetadata:name: hot-backupspec:hazelcastResourceName: my-hazelcastbucketURI: "s3://operator-backup"secret: "br-secret-s3"EOFExample URI → "s3://operator-backup?prefix=hazelcast/2022-06-08-17-01-20/"
-
Check the status of the backup:
bashkubectl get hotbackup hot-backupThe status of the backup is displayed in the output.
bashNAME STATUShot-backup Success
Restore from an external backup
-
Delete the Hazelcast cluster:
bashkubectl delete hazelcast my-hazelcast -
Delete the PersistentVolumeClaims.
The data is restored from external buckets, so you don't keep persistent volumes during a restore operation.
List PVCs that were created for the Hazelcast cluster:
bashkubectl get pvc -l "app.kubernetes.io/managed-by=hazelcast-platform-operator"NAME STATUS VOLUME CAPACITYpersistence-my-hazelcast-0 Bound pvc-a77fcd6e-e64d-4aeb-9708-835d66b4c5b9 8Gipersistence-my-hazelcast-1 Bound pvc-8548a4a5-fa8e-45bd-9909-8e6bb6a6c3e8 8Gipersistence-my-hazelcast-2 Bound pvc-c94edb25-5a72-482a-bf4e-84a99319d509 8GiDelete the PVCs:
bashkubectl delete pvc -l "app.kubernetes.io/managed-by=hazelcast-platform-operator" -
Create a new Hazelcast cluster.
For restoring, you will use the
HotBackupresource you have created.Create the Hazelcast cluster. Before the Hazelcast cluster is started, Operator starts the agent (as
InitContainer), which restores the backup data from the external bucket.shellkubectl apply -f - <<EOFapiVersion: hazelcast.com/v1alpha1kind: Hazelcastmetadata:name: my-hazelcastspec:clusterSize: 3licenseKeySecretName: hazelcast-license-keypersistence:pvc:accessModes: ["ReadWriteOnce"]requestStorage: 8Girestore:hotBackupResourceName: hot-backupexposeExternally:type: UnisocketdiscoveryServiceType: LoadBalancerEOFAs you may see, the agent configuration is not set. Operator therefore uses the latest stable version of the agent.
-
Check the cluster status:
shellkubectl get hazelcast my-hazelcastNAME STATUS MEMBERSmy-hazelcast Running 3/3After verifying that the cluster is
Runningand all the members are ready, find the discovery address:shellkubectl get hazelcastendpoint my-hazelcastNAME TYPE ADDRESSmy-hazelcast Discovery 34.28.159.226:5701Since we recreated the Hazelcast cluster, services are also recreated. The
ADDRESSmay change. -
Check the map size.
Configure the Hazelcast client to connect to the cluster external address as you did in Configure the Hazelcast Client.
Start the client to check the map size and check the restore is successful.
- CLC
- Java
- NodeJS
- Go
- Python
- .NET
bashclc -c hz map size --name persistent-mapbashcd javamvn packagejava -jar target/*jar-with-dependencies*.jar sizeYou should see the following output:
shellSuccessful connection!Current map size: 12bashcd nodejsnpm installnpm start sizeYou should see the following output:
shellSuccessful connection!Current map size: 12bashcd gogo run main.go sizeYou should see the following output:
shellSuccessful connection!Current map size: 12bashcd pythonpip install -r requirements.txtpython main.py sizeYou should see the following output:
shellSuccessful connection!Current map size: 12bashcd dotnetdotnet run sizeYou should see the following output:
shellSuccessful connection!Current map size: 12
Clean up
To clean up the created resources, remove all Custom Resources and PVCs.
kubectl delete secret <external-bucket-secret-name>
kubectl delete secret hazelcast-license-key
kubectl delete $(kubectl get hazelcast,hotbackup -o name)
kubectl delete pvc -l "app.kubernetes.io/managed-by=hazelcast-platform-operator"
Summary
In this tutorial, you learned how to:
- Deploy Hazelcast with persistence enabled.
- Create a Hazelcast map that has persistence enabled.
- Back up all map entries to external storage in the cloud.
- Restart the Hazelcast cluster and restore the backup map entries from the cloud.
Next steps
See also: