This the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Containers

La tecnologia per distribuire un'applicazione insieme con le dipendenze necessarie per la sua esecuzione.

Ogni container che viene eseguito è riproducibile; la pratica di includere le dipendenze all'interno di ciascuno container permette di ottenere sempre lo stesso risultato ad ogni esecuzione del medesimo container.

I Container permettono di disaccoppiare le applicazioni dall'infrastruttura del host su cui vengono eseguite. Questo approccio rende più facile il deployment su cloud o sitemi operativi differenti tra loro.

Immagine di container

L'immagine di un container e' un pacchetto software che contiene tutto ciò che serve per eseguire un'applicazione: il codice sorgente e ciascun runtime necessario, librerie applicative e di sistema, e le impostazioni predefinite per ogni configurazione necessaria.

Un container è immutabile per definizione: non è possibile modificare il codice di un container in esecuzione. Se si ha un'applicazione containerizzata e la si vuole modificare, si deve costruire un nuovo container che includa il cambiamento desiderato, e quindi ricreare il container partendo dalla nuova immagine aggiornata.

Container runtimes

Il container runtime è il software che è responsabile per l'esecuzione dei container.

Kubernetes supporta diversi container runtimes: Docker, containerd, cri-o, rktlet e tutte le implementazioni di Kubernetes CRI (Container Runtime Interface).

Voci correlate

1 - Immagini

L'immagine di un container rappresenta dati binari che incapsulano un'applicazione e tutte le sue dipendenze software. Le immagini sono costituite da pacchetti software eseguibili che possono essere avviati in modalità standalone e su cui si possono fare ipotesi ben precise circa l'ambiente in cui vengono eseguiti.

Tipicamente viene creata un'immagine di un'applicazione ed effettuato il push su un registry (un repository pubblico di immagini) prima di poterne fare riferimento esplicito in un Pod

Questa pagina va a delineare nello specifico il concetto di immagine di un container.

I nomi delle immagini

Alle immagini dei container vengono normalmente attribuiti nomi come pause, example/mycontainer, o kube-apiserver. Le immagini possono anche contenere l'hostname del registry in cui le immagini sono pubblicate; ad esempio: registro.fittizio.esempio/nomeimmagine, ed è possibile che sia incluso nel nome anche il numero della porta; ad esempio: registro.fittizio.esempio:10443/nomeimmagine.

Se non si specifica l'hostname di un registry, Kubernetes assume che ci si riferisca al registry pubblico di Docker.

Dopo la parte relativa al nome dell'immagine si può aggiungere un tag (come comunemente avviene per comandi come docker e podman). I tag permettono l'identificazione di differenti versioni della stessa serie di immagini.

I tag delle immagini sono composti da lettere minuscole e maiuscole, numeri, underscore (_), punti (.), e trattini (-).
Esistono regole aggiuntive relative a dove i caratteri separatori (_, -, and .) possano essere inseriti nel tag di un'immagine. Se non si specifica un tag, Kubernetes assume il tag latest che va a definire l'immagine disponibile più recente.

Attenzione:

Evitate di utilizzare il tag latest quando si rilasciano dei container in produzione, in quanto risulta difficile tracciare quale versione dell'immagine sia stata avviata e persino più difficile effettuare un rollback ad una versione precente.

Invece, meglio specificare un tag specifico come ad esempio v1.42.0.

Aggiornamento delle immagini

Quando un Deployment, StatefulSet, Pod, o qualsiasi altro oggetto che includa un Pod template viene creato per la prima volta, la policy di default per il pull di tutti i container nel Pod è impostata su IfNotPresent (se non presente) se non specificato diversamente. Questa policy permette al kubelet di evitare di fare il pull di un'immagine se questa è già presente.

Se necessario, si può forzare il pull in ogni occasione in uno dei seguenti modi:

  • impostando imagePullPolicy (specifica per il pull delle immagini) del container su Always (sempre).
  • omettendo imagePullPolicy ed usando il tag :latest (più recente) per l'immagine da utilizzare; Kubernetes imposterà la policy su Always (sempre).
  • omettendo imagePullPolicy ed il tag per l'immagine da utilizzare.
  • abilitando l'admission controller AlwaysPullImages.
Nota:

Il valore dell'impostazione imagePullPolicy del container è sempre presente quando l'oggetto viene creato per la prima volta e non viene aggiornato se il tag dell'immagine dovesse cambiare successivamente.

Ad esempio, creando un Deployment con un'immagine il cui tag non è :latest, e successivamente aggiornando il tag di quell'immagine a :latest, il campo imagePullPolicy non cambierà su Always. È necessario modificare manualmente la policy di pull di ogni oggetto dopo la sua creazione.

Quando imagePullPolicy è definito senza un valore specifico, esso è impostato su Always.

Multi-architecture support nelle immagini

Oltre a fornire immagini binarie, un container registry può fornire un indice delle immagini disponibili per un container. L'indice di un'immagine può puntare a più file manifest ciascuno per una versione specifica dell'architettura di un container. L'idea è che si può avere un unico nome per una stessa immagine (ad esempio: pause, example/mycontainer, kube-apiserver) e permettere a diversi sistemi di recuperare l'immagine binaria corretta a seconda dell'architettura della macchina che la sta utilizzando.

Kubernetes stesso tipicamente nomina le immagini dei container tramite il suffisso -$(ARCH). Per la garantire la retrocompatibilità è meglio generare le vecchie immagini con dei suffissi. L'idea è quella di generare, ad esempio, l'immagine pause con un manifest che include tutte le architetture supportate, affiancata, ad esempio, da pause-amd64 che è retrocompatibile per le vecchie configurazioni o per quei file YAML in cui sono specificate le immagini con i suffissi.

Utilizzare un private registry

I private registry possono richiedere l'utilizzo di chiavi per accedere alle immagini in essi contenute.
Le credenziali possono essere fornite in molti modi:

  • configurando i nodi in modo tale da autenticarsi al private registry
    • tutti i pod possono acquisire informazioni da qualsiasi private registry configurato
    • è necessario che l'amministratore del cluster configuri i nodi in tal senso
  • tramite pre-pulled images (immagini pre-caricate sui nodi)
    • tutti i pod possono accedere alle immagini salvate sulla cache del nodo a cui si riferiscono
    • è necessario effettuare l'accesso come root di sistema su ogni nodo per inserire questa impostazione
  • specificando ImagePullSecrets su un determinato pod
    • solo i pod che forniscono le proprie chiavi hanno la possibilità di accedere al private registry
  • tramite estensioni locali o specifiche di un Vendor
    • se si sta utilizzando una configurazione personalizzata del nodo oppure se manualmente, o tramite il cloud provider, si implementa un meccanismo di autenticazione del nodo presso il container registry.

Di seguito la spiegazione dettagliata di queste opzioni.

Configurazione dei nodi per l'autenticazione ad un private registry

Se si sta utilizzando Docker sui nodi, si può configurare il Docker container runtime per autenticare il nodo presso un private container registry.

Questo è un approccio possibile se si ha il controllo sulle configurazioni del nodo.

Nota: Kubernetes di default supporta solo le sezioni auths e HttpHeaders nelle configurazioni relative a Docker. Eventuali helper per le credenziali di Docker (credHelpers o credsStore) non sono supportati.

Docker salva le chiavi per i registri privati in $HOME/.dockercfg oppure nel file $HOME/.docker/config.json. Inserendo lo stesso file nella lista seguente, kubelet lo utilizzerà per recuperare le credenziali quando deve fare il pull delle immagini.

  • {--root-dir:-/var/lib/kubelet}/config.json
  • {cwd of kubelet}/config.json
  • ${HOME}/.docker/config.json
  • /.docker/config.json
  • {--root-dir:-/var/lib/kubelet}/.dockercfg
  • {cwd of kubelet}/.dockercfg
  • ${HOME}/.dockercfg
  • /.dockercfg
Nota: Potrebbe essere necessario impostare HOME=/root esplicitamente come variabile d'ambiente del processo kubelet.

Di seguito i passi consigliati per configurare l'utilizzo di un private registry da parte dei nodi del cluster. In questo esempio, eseguire i seguenti comandi sul proprio desktop/laptop:

  1. Esegui docker login [server] per ogni set di credenziali che vuoi utilizzare. Questo comando aggiornerà $HOME/.docker/config.json sul tuo PC.
  2. Controlla il file $HOME/.docker/config.json in un editor di testo per assicurarti che contenga le credenziali che tu voglia utilizzare.
  3. Recupera la lista dei tuoi nodi; ad esempio:
    • se vuoi utilizzare i nomi: nodes=$( kubectl get nodes -o jsonpath='{range.items[*].metadata}{.name} {end}' )
    • se vuoi recuperare gli indirizzi IP: nodes=$( kubectl get nodes -o jsonpath='{range .items[*].status.addresses[?(@.type=="ExternalIP")]}{.address} {end}' )
  4. Copia il tuo file locale .docker/config.json in uno dei path sopra riportati nella lista di ricerca.
    • ad esempio, per testare il tutto: for n in $nodes; do scp ~/.docker/config.json root@"$n":/var/lib/kubelet/config.json; done
Nota: Per i cluster di produzione, utilizza un configuration management tool per poter applicare le impostazioni su tutti i nodi laddove necessario.

Puoi fare una verifica creando un Pod che faccia uso di un'immagine privata; ad esempio:

kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: private-image-test-1
spec:
  containers:
    - name: uses-private-image
      image: $PRIVATE_IMAGE_NAME
      imagePullPolicy: Always
      command: [ "echo", "SUCCESS" ]
EOF
pod/private-image-test-1 created

Se tutto funziona correttamente, pochi istanti dopo, si può lanciare il comando:

kubectl logs private-image-test-1

e verificare che il comando restituisca in output:

SUCCESS

Qualora si sospetti che il comando sia fallito, si può eseguire:

kubectl describe pods/private-image-test-1 | grep 'Failed'

In caso di fallimento, l'output sarà simile al seguente:

  Fri, 26 Jun 2015 15:36:13 -0700    Fri, 26 Jun 2015 15:39:13 -0700    19    {kubelet node-i2hq}    spec.containers{uses-private-image}    failed        Failed to pull image "user/privaterepo:v1": Error: image user/privaterepo:v1 not found

Bisogna assicurarsi che tutti i nodi nel cluster abbiano lo stesso file .docker/config.json. Altrimenti i pod funzioneranno correttamente su alcuni nodi ma falliranno su altri. Ad esempio, se si utilizza l'autoscaling per i nodi, il template di ogni istanza devono includere il file .docker/config.json oppure montare un disco che lo contenga.

Tutti i pod avranno accesso in lettura alle immagini presenti nel private registry una volta che le rispettive chiavi di accesso siano state aggiunte nel file .docker/config.json.

Immagini pre-pulled

Nota: Questo approccio è possibile se si ha il controllo sulla configurazione del nodo. Non funzionerà qualora il cloud provider gestisca i nodi e li sostituisca automaticamente.

Kubelet di default prova a fare il pull di ogni immagine dal registry specificato. Tuttavia, qualora la proprietà imagePullPolicy (specifica di pull dell'immagine) del container sia impostata su IfNotPresent (vale a dire, se non è già presente) oppure su Never (mai), allora l'immagine locale è utilizzata (in via preferenziale o esclusiva, rispettivamente).

Se si vuole fare affidamento a immagini pre-scaricate per non dover incorrere in una fase di autenticazione presso il registry, bisogna assicurarsi che tutti i nodi nel cluster abbiano scaricato le stesse versioni delle immagini.

Questa procedura può essere utilizzata per accelerare il processo di creazione delle istanze o come alternativa all'autenticazione presso un private registry.

Tutti i pod avranno accesso in lettura a qualsiasi immagine pre-scaricata.

Specificare la proprietà imagePullSecrets su un Pod

Nota: Questo approccio è quello consigliato per l'avvio di container a partire da immagini presenti in registri privati.

Kubernetes da la possibilità di specificare le chiavi del container registry su un Pod.

Creare un Secret tramite Docker config

Esegui il comando seguente, sostituendo i valori riportati in maiuscolo con quelli corretti:

kubectl create secret docker-registry <name> --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL

Se possiedi il file delle credenziali per Docker, anziché utilizzare il comando quì sopra puoi importare il file di credenziali come un Kubernetes Secrets.
Creare un Secret a partire da credenziali Docker fornisce la spiegazione dettagliata su come fare.

Ciò è particolarmente utile se si utilizzano più container registry privati, in quanto il comando kubectl create secret docker-registry genera un Secret che funziona con un solo private registry.

Nota: I Pod possono fare riferimento ai Secret per il pull delle immagini soltanto nel proprio namespace, quindi questo procedimento deve essere svolto per ogni namespace.

Fare riferimento ad imagePullSecrets in un Pod

È possibile creare pod che referenzino quel Secret aggiungendo la sezione imagePullSecrets alla definizione del Pod.

Ad esempio:

cat <<EOF > pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: foo
  namespace: awesomeapps
spec:
  containers:
    - name: foo
      image: janedoe/awesomeapp:v1
  imagePullSecrets:
    - name: myregistrykey
EOF

cat <<EOF >> ./kustomization.yaml
resources:
- pod.yaml
EOF

Questo deve esser fatto per ogni Pod che utilizzi un private registry.

Comunque, le impostazioni relative a questo campo possono essere automatizzate inserendo la sezione imagePullSecrets nella definizione della risorsa ServiceAccount.

Visitare la pagina Aggiungere ImagePullSecrets ad un Service Account per istruzioni più dettagliate.

Puoi utilizzarlo in congiunzione al file .docker/config.json configurato per ogni nodo. In questo caso, si applicherà un merge delle credenziali.

Casi d'uso

Ci sono varie soluzioni per configurare i private registry. Di seguito, alcuni casi d'uso comuni e le soluzioni suggerite.

  1. Cluster in cui sono utilizzate soltanto immagini non proprietarie (ovvero open-source). In questo caso non sussiste il bisogno di nascondere le immagini.
    • Utilizza immagini pubbliche da Docker hub.
      • Nessuna configurazione richiesta.
      • Alcuni cloud provider mettono in cache o effettuano il mirror di immagini pubbliche, il che migliora la disponibilità delle immagini e ne riduce il tempo di pull.
  2. Cluster con container avviati a partire da immagini proprietarie che dovrebbero essere nascoste a chi è esterno all'organizzazione, ma visibili a tutti gli utenti abilitati nel cluster.
    • Utilizza un private Docker registry.
      • Esso può essere ospitato da Docker Hub, o da qualche altra piattaforma.
      • Configura manualmente il file .docker/config.json su ogni nodo come descritto sopra.
    • Oppure, avvia un private registry dietro il tuo firewall con accesso in lettura libero.
      • Non è necessaria alcuna configurazione di Kubernetes.
    • Utilizza un servizio di container registry che controlli l'accesso alle immagini
      • Esso funzionerà meglio con una configurazione del cluster basata su autoscaling che con una configurazione manuale del nodo.
    • Oppure, su un cluster dove la modifica delle configurazioni del nodo non è conveniente, utilizza imagePullSecrets.
  3. Cluster con immagini proprietarie, alcune delle quali richiedono un controllo sugli accessi.
    • Assicurati che l'admission controller AlwaysPullImages sia attivo. Altrimenti, tutti i Pod potenzialmente possono avere accesso a tutte le immagini.
    • Sposta i dati sensibili un un Secret, invece di inserirli in un'immagine.
  4. Un cluster multi-tenant dove ogni tenant necessiti di un private registry.
    • Assicurati che l'admission controller AlwaysPullImages sia attivo. Altrimenti, tutti i Pod di tutti i tenant potrebbero potenzialmente avere accesso a tutte le immagini.
    • Avvia un private registry che richieda un'autorizzazione all'accesso.
    • Genera delle credenziali di registry per ogni tenant, inseriscile in dei Secret, e popola i Secret per ogni namespace relativo ad ognuno dei tenant.
    • Il singolo tenant aggiunge così quel Secret all'impostazione imagePullSecrets di ogni namespace.

Se si ha la necessità di accedere a più registri, si può generare un Secret per ognuno di essi. Kubelet farà il merge di ogni imagePullSecrets in un singolo file virtuale .docker/config.json.

Voci correlate

2 - Container Environment

Questa pagina descrive le risorse disponibili nei Container eseguiti in Kubernetes.

Container environment

Quando si esegue un Container in Kubernetes, le seguenti risorse sono rese disponibili:

  • Un filesystem, composto dal file system dell'image e da uno o più volumes.
  • Una serie di informazioni sul Container stesso.
  • Una serie di informazioni sugli oggetti nel cluster.

Informazioni sul Container

L' hostname di un Container è il nome del Pod all'interno del quale è eseguito il Container. È consultabile tramite il comando hostname o tramite la funzione gethostname disponibile in libc.

Il nome del Pod e il namespace possono essere resi disponibili come environment variables attraverso l'uso delle downward API.

Gli utenti possono aggiungere altre environment variables nella definizione del Pod; anche queste saranno disponibili nel Container come tutte le altre environment variables definite staticamente nella Docker image.

Informazioni sul cluster

Al momento della creazione del Container è generata una serie di environment variables con la lista di servizi in esecuzione nel cluster. Queste environment variables rispettano la sintassi dei Docker links.

Per un servizio chiamato foo che è in esecuzione in un Container di nome bar, le seguenti variabili sono generate:

FOO_SERVICE_HOST=<host su cui il servizio è attivo>
FOO_SERVICE_PORT=<porta su cui il servizio è pubblicato>

I servizi hanno un indirizzo IP dedicato e sono disponibili nei Container anche via DNS se il DNS addon è installato nel cluster.

Voci correlate

3 - Container Lifecycle Hooks

Questa pagina descrive come i Container gestiti con kubelet possono utilizzare il lifecycle hook framework dei Container per l'esecuzione di codice eseguito in corrispondenza di alcuni eventi durante il loro ciclo di vita.

Overview

Analogamente a molti framework di linguaggi di programmazione che hanno degli hooks legati al ciclo di vita dei componenti, come ad esempio Angular, Kubernetes fornisce ai Container degli hook legati al loro ciclo di vita dei Container. Gli hook consentono ai Container di essere consapevoli degli eventi durante il loro ciclo di gestione ed eseguire del codice implementato in un handler quando il corrispondente hook viene eseguito.

Container hooks

Esistono due tipi di hook che vengono esposti ai Container:

PostStart

Questo hook viene eseguito successivamente alla creazione del container. Tuttavia, non vi è garanzia che questo hook venga eseguito prima dell'ENTRYPOINT del container. Non vengono passati parametri all'handler.

PreStop

Questo hook viene eseguito prima della terminazione di un container a causa di una richiesta API o di un evento di gestione, come ad esempio un fallimento delle sonde di liveness/startup, preemption, risorse contese e altro. Una chiamata all'hook di PreStop fallisce se il container è in stato terminated o completed e l'hook deve finire prima che possa essere inviato il segnale di TERM per fermare il container. Il conto alla rovescia per la terminazione del Pod (grace period) inizia prima dell'esecuzione dell'hook PreStop, quindi indipendentemente dall'esito dell'handler, il container terminerà entro il grace period impostato. Non vengono passati parametri all'handler.

Una descrizione più dettagliata riguardante al processo di terminazione dei Pod può essere trovata in Terminazione dei Pod.

Implementazione degli hook handler

I Container possono accedere a un hook implementando e registrando un handler per tale hook. Ci sono due tipi di handler che possono essere implementati per i Container:

  • Exec - Esegue un comando specifico, tipo pre-stop.sh, all'interno dei cgroup e namespace del Container. Le risorse consumate dal comando vengono contate sul Container.
  • HTTP - Esegue una richiesta HTTP verso un endpoint specifico del Container.

Esecuzione dell'hook handler

Quando viene richiamato l'hook legato al lifecycle del Container, il sistema di gestione di Kubernetes esegue l'handler secondo l'azione dell'hook, httpGet e tcpSocket vengono eseguiti dal processo kubelet, mentre exec è eseguito nel Container.

Le chiamate agli handler degli hook sono sincrone rispetto al contesto del Pod che contiene il Container. Questo significa che per un hook PostStart, l'ENTRYPOINT e l'hook si attivano in modo asincrono. Tuttavia, se l'hook impiega troppo tempo per essere eseguito o si blocca, il container non può raggiungere lo stato di running.

Gli hook di PreStop non vengono eseguiti in modo asincrono dall'evento di stop del container; l'hook deve completare la sua esecuzione prima che l'evento TERM possa essere inviato. Se un hook di PreStop si blocca durante la sua esecuzione, la fase del Pod rimarrà Terminating finchè il Pod non sarà rimosso forzatamente dopo la scadenza del suo terminationGracePeriodSeconds. Questo grace period si applica al tempo totale necessario per effettuare sia l'esecuzione dell'hook di PreStop che per l'arresto normale del container. Se, per esempio, il terminationGracePeriodSeconds è di 60, e l'hook impiega 55 secondi per essere completato, e il container impiega 10 secondi per fermarsi normalmente dopo aver ricevuto il segnale, allora il container verrà terminato prima di poter completare il suo arresto, poiché terminationGracePeriodSeconds è inferiore al tempo totale (55+10) necessario perché queste due cose accadano.

Se un hook PostStart o PreStop fallisce, allora il container viene terminato.

Gli utenti dovrebbero mantenere i loro handler degli hook i più leggeri possibili. Ci sono casi, tuttavia, in cui i comandi di lunga durata hanno senso, come il salvataggio dello stato del container prima della sua fine.

Garanzia della chiamata dell'hook

La chiamata degli hook avviene almeno una volta, il che significa che un hook può essere chiamato più volte da un dato evento, come per PostStart o PreStop. Sta all'implementazione dell'hook gestire correttamente questo aspetto.

Generalmente, vengono effettuate singole chiamate agli hook. Se, per esempio, la destinazione di hook HTTP non è momentaneamente in grado di ricevere traffico, non c'è alcun tentativo di re invio. In alcuni rari casi, tuttavia, può verificarsi una doppia chiamata. Per esempio, se un kubelet si riavvia nel mentre dell'invio di un hook, questo potrebbe essere chiamato per una seconda volta dopo che il kubelet è tornato in funzione.

Debugging Hook handlers

I log di un handler di hook non sono esposti negli eventi del Pod. Se un handler fallisce per qualche ragione, trasmette un evento. Per il PostStart, questo è l'evento di FailedPostStartHook, e per il PreStop, questo è l'evento di FailedPreStopHook. Puoi vedere questi eventi eseguendo kubectl describe pod <pod_name>. Ecco alcuni esempi di output di eventi dall'esecuzione di questo comando:

Events:
  FirstSeen  LastSeen  Count  From                                                   SubObjectPath          Type      Reason               Message
  ---------  --------  -----  ----                                                   -------------          --------  ------               -------
  1m         1m        1      {default-scheduler }                                                          Normal    Scheduled            Successfully assigned test-1730497541-cq1d2 to gke-test-cluster-default-pool-a07e5d30-siqd
  1m         1m        1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Pulling              pulling image "test:1.0"
  1m         1m        1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Created              Created container with docker id 5c6a256a2567; Security:[seccomp=unconfined]
  1m         1m        1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Pulled               Successfully pulled image "test:1.0"
  1m         1m        1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Started              Started container with docker id 5c6a256a2567
  38s        38s       1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Killing              Killing container with docker id 5c6a256a2567: PostStart handler: Error executing in Docker Container: 1
  37s        37s       1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Killing              Killing container with docker id 8df9fdfd7054: PostStart handler: Error executing in Docker Container: 1
  38s        37s       2      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}                         Warning   FailedSync           Error syncing pod, skipping: failed to "StartContainer" for "main" with RunContainerError: "PostStart handler: Error executing in Docker Container: 1"
  1m         22s       2      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Warning   FailedPostStartHook

Voci correlate