diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7f12c96eca0f2fe74bbae895390c5aba93cc9c10..4525a78c1901fde1b43c44d2b09f9d80cdf67e63 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -28,6 +28,6 @@ include:
   - local: '/build/ci/.security-and-compliance-ci.yml'
   - local: '/build/ci/.build-container.yml'
   - local: '/build/ci/.test.yml'
-  - local: '/build/ci/.terraform-ci.yml'
+  - local: '/build/ci/.containerlab-ci.yml'
   - local: '/build/ci/.deploy-k8s.yml'
   - local: '/build/ci/.uml-autogen-ci.yml'
diff --git a/build/ci/.build-container.yml b/build/ci/.build-container.yml
index 12dfb819df09e377633a89e1c7db23484925b2a0..d35f1c6a2d6a8d0dcae2dcbe2a1b807686ae2786 100644
--- a/build/ci/.build-container.yml
+++ b/build/ci/.build-container.yml
@@ -12,7 +12,6 @@ variables:
   tags:
     - dind
   script:
-    - docker info
     - >
       docker build \
         --build-arg GITLAB_USER=$GO_MODULES_USER \
diff --git a/build/ci/.containerlab-ci.yml b/build/ci/.containerlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dd69edc646669eec082e1974340839cc9f99dd47
--- /dev/null
+++ b/build/ci/.containerlab-ci.yml
@@ -0,0 +1,81 @@
+
+variables:
+  CEOS_CONTAINER_IMAGE: "$CI_REGISTRY_IMAGE/ceos:latest"
+  CLAB_INT1_TEMPLATE: "${CI_PROJECT_DIR}/test/containerlab/int01.clab.yml"
+  CLAB_NAME: "clab${CI_PIPELINE_IID}"
+  CLAB_DIR: "/mnt"
+
+
+.containerlab_rules: &containerlab_rules
+  rules:
+    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop')
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+    - if: $CI_NIGHTLY
+
+
+.containerlab_template: &containerlab_template
+  tags:
+    - shell
+  before_script:
+    - cd ${CLAB_DIR}
+    - echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
+    - echo $DOCKER_IMAGE_SHA
+    - docker pull $DOCKER_IMAGE_SHA
+    - docker pull ${CEOS_CONTAINER_IMAGE}
+
+
+containerlab:template:
+  extends: .containerlab_rules
+  image: alpine:latest
+  stage: build
+  before_script:
+    - echo "Override global before_script"
+  script:
+    - ./build/ci/generate_octet.sh $CI_COMMIT_SHA >> $(pwd)/firstOctet
+    - ./build/ci/generate_octet.sh $CI_PIPELINE_ID >> $(pwd)/secondOctet
+    - export firstOctet=$(cat $(pwd)/firstOctet)
+    - export secondOctet=$(cat $(pwd)/secondOctet)
+    - export CLAB_MGMT_SUBNET="172.$firstOctet.$secondOctet.0/24"
+    - |
+      sed -e "s|@@CEOS_CONTAINER_IMAGE@@|${CEOS_CONTAINER_IMAGE}|g" \
+          -e "s|@@GOSDN_CONTAINER_IMAGE@@|${DOCKER_IMAGE_SHA}|g" \
+          -e "s|@@CLAB_NAME@@|${CLAB_NAME}|g" \
+          -e "s|@@CLAB_MGMT_SUBNET@@|${CLAB_MGMT_SUBNET}|g" \
+          ${CLAB_INT1_TEMPLATE} > ${CI_PROJECT_DIR}/${CLAB_NAME}.clab.yml
+    - cat ${CLAB_NAME}.clab.yml
+  artifacts:
+    name: ${CLAB_NAME}
+    paths:
+      - ${CI_PROJECT_DIR}/${CLAB_NAME}.clab.yml
+
+
+containerlab:deploy:
+  extends:
+    - .containerlab_template
+    - .containerlab_rules
+  stage: apply
+  script:
+    - sudo containerlab deploy --topo ${CI_PROJECT_DIR}/${CLAB_NAME}.clab.yml --reconfigure
+    - echo "GOSDN_HTTP_PORT=$(docker inspect -f '{{ (index (index .NetworkSettings.Ports "8080/tcp") 0).HostPort }}' clab-${CLAB_NAME}-gosdn)" >> ${CI_PROJECT_DIR}/build.env
+    - echo "GOSDN_GRPC_PORT=$(docker inspect -f '{{ (index (index .NetworkSettings.Ports "55055/tcp") 0).HostPort }}' clab-${CLAB_NAME}-gosdn)" >> ${CI_PROJECT_DIR}/build.env
+    - echo "CEOS1_PORT=$(docker inspect -f '{{ (index (index .NetworkSettings.Ports "6030/tcp") 0).HostPort }}' clab-${CLAB_NAME}-ceos1)" >> ${CI_PROJECT_DIR}/build.env
+  dependencies:
+    - containerlab:template
+  artifacts:
+    reports:
+      dotenv: ${CI_PROJECT_DIR}/build.env
+
+containerlab:destroy:
+  stage: .post
+  tags:
+    - shell
+  before_script:
+    - cd ${CLAB_DIR}
+  script:
+    - sudo containerlab destroy --topo ${CI_PROJECT_DIR}/${CLAB_NAME}.clab.yml
+    - docker volume rm -f ${CLAB_NAME}-volume
+    - docker image rm -f ${DOCKER_IMAGE_SHA}
+  allow_failure: true
+  dependencies:
+    - containerlab:template
+  when: always
diff --git a/build/ci/.deploy-k8s.yml b/build/ci/.deploy-k8s.yml
index 37c76a364c89c6993d22708998054017f7f0322c..9642ce0cac4f6cd0fc379fda9921c9e30f9fe61f 100644
--- a/build/ci/.deploy-k8s.yml
+++ b/build/ci/.deploy-k8s.yml
@@ -4,7 +4,6 @@ build:k8s-bot:
   rules:
     - if: $CI_COMMIT_BRANCH == "develop"
     - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
-    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
   script:
     - cd build/cd
     - go build -o k8s-bot
@@ -24,16 +23,6 @@ build:k8s-bot:
   script:
     - ./build/cd/k8s-bot
 
-deploy:integration-test:
-  <<: *deploy
-  stage: apply
-  needs:
-    - job: "build:merge-request"
-    - job: "build:k8s-bot"
-      artifacts: true
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
-
 deploy:develop:
   <<: *deploy
   stage: deploy
@@ -73,20 +62,3 @@ deploy:nightly:develop:
       artifacts: true
   rules:
     - if: $CI_COMMIT_BRANCH == "develop" && $CI_NIGHTLY == "mainline"
-
-destroy:k8s:
-  image: 
-    name: bitnami/kubectl:latest
-    entrypoint: [""]
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
-      when: always
-  before_script:
-    - echo "override global before script"
-  stage: .post
-  variables:
-    K8S_OP: "delete"
-  script:
-    - ./build/cd/k8s-bot
-  dependencies:
-    - build:k8s-bot
diff --git a/build/ci/.terraform-ci.yml b/build/ci/.terraform-ci.yml
deleted file mode 100644
index 04edfa70499fda5113086ad8c8308e593fa453fb..0000000000000000000000000000000000000000
--- a/build/ci/.terraform-ci.yml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-variables:
-  TF_ROOT: ${CI_PROJECT_DIR}/test/terraform
-  TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_PIPELINE_ID}
-  DOCKER_IMAGE_SHA: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
-
-cache:
-  key: ${CI_PIPELINE_ID}
-  paths:
-    - ${TF_ROOT}/.terraform
-
-.terraform_prefab: &tf
-  image: registry.gitlab.com/gitlab-org/terraform-images/stable:latest
-  variables:
-    CI_DEBUG_TRACE: "false"
-  before_script:
-    - ./build/ci/generate_octet.sh $CI_COMMIT_SHA >> ${TF_ROOT}/firstOctet
-    - ./build/ci/generate_octet.sh $CI_PIPELINE_ID >> ${TF_ROOT}/secondOctet
-    - cd ${TF_ROOT}
-    - export TF_VAR_integration_username=terraform
-    - export TF_VAR_integration_access_token=${TERRAFORM_API_TOKEN}
-    - export TF_VAR_integration_registry=${CI_REGISTRY}
-    - export TF_VAR_ceos_tag=registry.code.fbi.h-da.de/cocsn/gosdn/ceos:${CI_PIPELINE_ID}
-    - export TF_VAR_container_tag=registry.code.fbi.h-da.de/cocsn/gosdn:${CI_PIPELINE_ID}
-    - export TF_VAR_tls_key=${DOCKER_TLS_KEY}
-    - export TF_VAR_tls_cert=${DOCKER_TLS_CERT}
-    - export TF_VAR_tls_ca_cert=${DOCKER_TLS_CA}
-    - export TF_VAR_ceos_address=172.24.$(cat firstOctet).$(cat secondOctet)
-    - export TF_VAR_gosdn_address=172.24.$(cat secondOctet).$(cat firstOctet)
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop')
-    - if: $CI_COMMIT_BRANCH == "integration-test"
-    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
-    - if: $CI_NIGHTLY
-
-init:
-  stage: .pre
-  script:
-    - gitlab-terraform init
-  <<: *tf
-
-tag-images:
-  stage: .pre
-  before_script:
-    - echo "override global before script"
-  image: docker:19.03.12
-  tags:
-    - dind
-  services:
-  - name: docker:19.03.12-dind
-    command: ["--registry-mirror", "http://141.100.70.170:6000", "--dns", "1.1.1.1"]
-  variables:
-    DOCKER_TLS_CERTDIR: "/certs"
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop')
-      variables:
-        TF_VAR_container_tag: $DOCKER_IMAGE_SHA
-    - if: $CI_COMMIT_BRANCH == "integration-test"
-    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
-    - if: $CI_NIGHTLY
-  script:
-    - docker info
-    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
-    - docker pull registry.code.fbi.h-da.de/cocsn/gosdn/ceos:latest
-    - docker tag registry.code.fbi.h-da.de/cocsn/gosdn/ceos:latest registry.code.fbi.h-da.de/cocsn/gosdn/ceos:${CI_PIPELINE_ID}
-
-validate:
-  stage: test
-  script:
-    - gitlab-terraform validate
-  needs: ["init"]
-  <<: *tf
-
-plan:
-  before_script:
-    - cd ${TF_ROOT}
-  stage: build
-  script:
-    - gitlab-terraform plan
-    - gitlab-terraform plan-json
-  artifacts:
-    name: plan
-    paths:
-      - ${TF_ROOT}/plan.cache
-    reports:
-      terraform: ${TF_ROOT}/plan.json
-  needs: ["validate"]
-  <<: *tf
-
-apply:
-  stage: apply
-  script:
-    - gitlab-terraform apply
-  dependencies:
-    - plan
-  <<: *tf
-
-destroy:tf:
-  stage: .post
-  script:
-    - gitlab-terraform destroy
-  <<: *tf
diff --git a/build/ci/.test.yml b/build/ci/.test.yml
index 712efe07fdcb05276521725aa27877e7b7dae037..4ba4ab6c9081c026f26e5451c77bd84e390724a3 100644
--- a/build/ci/.test.yml
+++ b/build/ci/.test.yml
@@ -2,30 +2,23 @@
   image: golang:1.16
   stage: integration-test
   needs:
-    - job: "apply"
-    - job: "deploy:integration-test"
-
+    - job: "containerlab:deploy"
   variables:
     GOSDN_LOG: "nolog"
-    GOSDN_CHANGE_TIMEOUT: "100ms"
+    GOSDN_TEST_API_ENDPOINT: "141.100.70.178:${GOSDN_GRPC_PORT}"
+    GOSDN_TEST_ENDPOINT: "141.100.70.178:${CEOS1_PORT}"
+    GOSDN_TEST_USER: "admin"
+    GOSDN_TEST_PASSWORD: "admin"
   rules:
     - if: $CI_NIGHTLY
-      when: delayed
-      start_in: 2 minutes
     - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH
-      when: delayed
-      start_in: 2 minutes
     - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != $CI_DEFAULT_BRANCH
       allow_failure: true
-      when: delayed
-      start_in: 2 minutes
 
 integration-test:nucleus:
   <<: *integration-test
   script:
-    - ./build/ci/generate_octet.bash $CI_COMMIT_SHA >> firstOctet
-    - ./build/ci/generate_octet.bash $CI_PIPELINE_ID >> secondOctet
-    - export GOSDN_TEST_ENDPOINT=172.24.$(cat firstOctet).$(cat secondOctet):6030
+    - ${CI_PROJECT_DIR}/build/ci/wait-for-it.sh ${GOSDN_TEST_ENDPOINT} -s -t 180 -- echo "CEOS is up"
     - cd ./test/integration
     - go test -race -v -run TestGnmi_SetIntegration
     - go test -race -v -run TestGnmi_GetIntegration
@@ -36,16 +29,7 @@ integration-test:api:
   <<: *integration-test
   variables:
     K8S_OP: "getenv"
-  needs:
-    - job: "build:merge-request"
-    - job: "apply"
-    - job: "deploy:integration-test"
-    - job: "build:k8s-bot"
-      artifacts: true
   script:
-    - ./build/ci/generate_octet.bash $CI_COMMIT_SHA >> firstOctet
-    - ./build/ci/generate_octet.bash $CI_PIPELINE_ID >> secondOctet
-    - export GOSDN_TEST_API_ENDPOINT=172.24.$(cat secondOctet).$(cat firstOctet):55055
     - cd ./api
     - go test -race -v -run TestApiIntegration
 
@@ -74,3 +58,8 @@ controller-test:
   script:
     - go test -race -v -run TestRun
   <<: *test
+
+controller-test:
+  script:
+    - go test -race -v -run TestRun
+  <<: *test
diff --git a/build/ci/wait-for-it.sh b/build/ci/wait-for-it.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d990e0d364f576ee83cd699707076ca49ad36a4d
--- /dev/null
+++ b/build/ci/wait-for-it.sh
@@ -0,0 +1,182 @@
+#!/usr/bin/env bash
+# Use this script to test if a given TCP host/port are available
+
+WAITFORIT_cmdname=${0##*/}
+
+echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+    cat << USAGE >&2
+Usage:
+    $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
+    -h HOST | --host=HOST       Host or IP under test
+    -p PORT | --port=PORT       TCP port under test
+                                Alternatively, you specify the host and port as host:port
+    -s | --strict               Only execute subcommand if the test succeeds
+    -q | --quiet                Don't output any status messages
+    -t TIMEOUT | --timeout=TIMEOUT
+                                Timeout in seconds, zero for no timeout
+    -- COMMAND ARGS             Execute command with args after the test finishes
+USAGE
+    exit 1
+}
+
+wait_for()
+{
+    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+        echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+    else
+        echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
+    fi
+    WAITFORIT_start_ts=$(date +%s)
+    while :
+    do
+        if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
+            nc -z $WAITFORIT_HOST $WAITFORIT_PORT
+            WAITFORIT_result=$?
+        else
+            (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
+            WAITFORIT_result=$?
+        fi
+        if [[ $WAITFORIT_result -eq 0 ]]; then
+            WAITFORIT_end_ts=$(date +%s)
+            echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
+            break
+        fi
+        sleep 1
+    done
+    return $WAITFORIT_result
+}
+
+wait_for_wrapper()
+{
+    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+    if [[ $WAITFORIT_QUIET -eq 1 ]]; then
+        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+    else
+        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+    fi
+    WAITFORIT_PID=$!
+    trap "kill -INT -$WAITFORIT_PID" INT
+    wait $WAITFORIT_PID
+    WAITFORIT_RESULT=$?
+    if [[ $WAITFORIT_RESULT -ne 0 ]]; then
+        echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+    fi
+    return $WAITFORIT_RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+    case "$1" in
+        *:* )
+        WAITFORIT_hostport=(${1//:/ })
+        WAITFORIT_HOST=${WAITFORIT_hostport[0]}
+        WAITFORIT_PORT=${WAITFORIT_hostport[1]}
+        shift 1
+        ;;
+        --child)
+        WAITFORIT_CHILD=1
+        shift 1
+        ;;
+        -q | --quiet)
+        WAITFORIT_QUIET=1
+        shift 1
+        ;;
+        -s | --strict)
+        WAITFORIT_STRICT=1
+        shift 1
+        ;;
+        -h)
+        WAITFORIT_HOST="$2"
+        if [[ $WAITFORIT_HOST == "" ]]; then break; fi
+        shift 2
+        ;;
+        --host=*)
+        WAITFORIT_HOST="${1#*=}"
+        shift 1
+        ;;
+        -p)
+        WAITFORIT_PORT="$2"
+        if [[ $WAITFORIT_PORT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --port=*)
+        WAITFORIT_PORT="${1#*=}"
+        shift 1
+        ;;
+        -t)
+        WAITFORIT_TIMEOUT="$2"
+        if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --timeout=*)
+        WAITFORIT_TIMEOUT="${1#*=}"
+        shift 1
+        ;;
+        --)
+        shift
+        WAITFORIT_CLI=("$@")
+        break
+        ;;
+        --help)
+        usage
+        ;;
+        *)
+        echoerr "Unknown argument: $1"
+        usage
+        ;;
+    esac
+done
+
+if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
+    echoerr "Error: you need to provide a host and port to test."
+    usage
+fi
+
+WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
+WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
+WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
+WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
+
+# Check to see if timeout is from busybox?
+WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
+WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
+
+WAITFORIT_BUSYTIMEFLAG=""
+if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
+    WAITFORIT_ISBUSY=1
+    # Check if busybox timeout uses -t flag
+    # (recent Alpine versions don't support -t anymore)
+    if timeout &>/dev/stdout | grep -q -e '-t '; then
+        WAITFORIT_BUSYTIMEFLAG="-t"
+    fi
+else
+    WAITFORIT_ISBUSY=0
+fi
+
+if [[ $WAITFORIT_CHILD -gt 0 ]]; then
+    wait_for
+    WAITFORIT_RESULT=$?
+    exit $WAITFORIT_RESULT
+else
+    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+        wait_for_wrapper
+        WAITFORIT_RESULT=$?
+    else
+        wait_for
+        WAITFORIT_RESULT=$?
+    fi
+fi
+
+if [[ $WAITFORIT_CLI != "" ]]; then
+    if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
+        echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
+        exit $WAITFORIT_RESULT
+    fi
+    exec "${WAITFORIT_CLI[@]}"
+else
+    exit $WAITFORIT_RESULT
+fi
diff --git a/test/containerlab/int01.clab.yml b/test/containerlab/int01.clab.yml
new file mode 100644
index 0000000000000000000000000000000000000000..10512cce11f3c85331c695cfe5c7aed1dfff67f3
--- /dev/null
+++ b/test/containerlab/int01.clab.yml
@@ -0,0 +1,23 @@
+# topology documentation: http://containerlab.srlinux.dev/lab-examples/srl-ceos/
+name: @@CLAB_NAME@@
+
+mgmt:
+  network: @@CLAB_NAME@@
+  ipv4_subnet: @@CLAB_MGMT_SUBNET@@
+
+topology:
+  kinds:
+    ceos:
+      image: @@CEOS_CONTAINER_IMAGE@@
+  nodes:
+    ceos1:
+      kind: ceos
+      ports:
+        - 0:6030
+
+    gosdn:
+      kind: linux
+      image: @@GOSDN_CONTAINER_IMAGE@@
+      ports:
+        - 0:8080
+        - 0:55055