{"id":88,"date":"2025-12-30T12:00:00","date_gmt":"2025-12-30T12:00:00","guid":{"rendered":"https:\/\/rezachegini.com\/?p=88"},"modified":"2026-10-30T06:52:06","modified_gmt":"2026-10-30T06:52:06","slug":"gitlab-ci-cd-container-native-pipelines-with-built-in-sast","status":"publish","type":"post","link":"https:\/\/rezachegini.com\/?p=88","title":{"rendered":"GitLab CI\/CD : Container-Native Pipelines with Built-in SAST"},"content":{"rendered":"<h3 class=\"wp-block-heading\">Introduction: Why I Switched from Jenkins to GitLab<\/h3>\n<p class=\"wp-block-paragraph\">After building Jenkins pipelines, I was comfortable with Jenkins. Then I discovered GitLab CI\/CD and everything changed.<\/p>\n<p class=\"wp-block-paragraph\">The fundamental difference? GitLab is container-native from day one.<\/p>\n<p class=\"wp-block-paragraph\">Jenkins approach:<\/p>\n<ul class=\"wp-block-list\"><li>Install Jenkins on server<\/li><li>Configure agents with Java, Maven, Node.js, Python<\/li><li>Install plugins for Docker, SonarQube, JUnit<\/li><li>Write Groovy pipeline code<\/li><\/ul>\n<p class=\"wp-block-paragraph\">GitLab approach:<\/p>\n<ul class=\"wp-block-list\"><li>Install GitLab Runner<\/li><li>Install Docker<\/li><li>Write YAML<\/li><li>Everything else runs in containers<\/li><\/ul>\n<p class=\"wp-block-paragraph\">This article shows my complete migration with deep technical explanations of how GitLab&#8217;s container-native architecture works internally.<\/p>\n<h3 class=\"wp-block-heading\">GitLab CI\/CD Architecture: How It Actually Works<\/h3>\n<h3 class=\"wp-block-heading\">The Component Stack<\/h3>\n<pre class=\"wp-block-code\"><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502         GitLab Server               \u2502\n\u2502  (Manages jobs, stores artifacts)   \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n               \u2502\n               \u2502 HTTPS\/gRPC\n               \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502       GitLab Runner                 \u2502\n\u2502  (Executes jobs, pulls containers)  \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n               \u2502\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u2502             \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Docker     \u2502 \u2502   Shell    \u2502\n\u2502  Executor   \u2502 \u2502  Executor  \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Communication flow:<\/p>\n<ol class=\"wp-block-list\"><li>GitLab Server (gitlab.com or self-hosted):<\/li><li>GitLab Runner (your machine\/VM):<\/li><li>Executors:<\/li><\/ol>\n<h3 class=\"wp-block-heading\">My First GitLab Pipeline: Deep Technical Analysis<\/h3>\n<p class=\"wp-block-paragraph\">Repository: GitLab\/java-project\/java-project.yml<\/p>\n<pre class=\"wp-block-code\"><code>stages:\n- maven-test-stage\n- maven-deploy-stage\n- sonarqube-stage\n- docker-build-stage\n- docker-push-stage\n- docker-run-stage\n- aws-configuration-stage\n- kubernetes-stage\n\nmaven-test-job:\n  stage: maven-test-stage\n  tags:\n  - macos\n  - personal\n  - shell\n  before_script:\n  - mvn -v\n  script:\n  - mvn clean package\n  - mvn test\n  artifacts:\n    name: war\n    paths:\n    - \"target\/my-webapp.war\"\n    reports:\n      junit:\n      - target\/surefire-reports\/TEST.*.xml\n    expire_in: 30 days<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Deep Dive: Tags and Runner Selection<\/h3>\n<pre class=\"wp-block-code\"><code>tags:\n- macos\n- personal\n- shell<\/code><\/pre>\n<p class=\"wp-block-paragraph\">How runner selection works:<\/p>\n<p class=\"wp-block-paragraph\">Step 1: Job enters queue<\/p>\n<p class=\"wp-block-paragraph\">When pipeline starts, GitLab creates job with these requirements:<\/p>\n<pre class=\"wp-block-code\"><code>{\n  \"job_id\": 12345,\n  \"tags_required\": [\"macos\", \"personal\", \"shell\"],\n  \"image\": null,\n  \"services\": []\n}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 2: Runners check eligibility<\/p>\n<p class=\"wp-block-paragraph\">Every 3 seconds, each runner asks GitLab: &#8220;Do you have jobs for me?&#8221;<\/p>\n<pre class=\"wp-block-code\"><code>Runner A (Linux, tags: [linux, docker]):\n- Has tag \"macos\"? NO\n- Skip this job\n\nRunner B (macOS, tags: [macos, personal, shell]):\n- Has tag \"macos\"? YES\n- Has tag \"personal\"? YES\n- Has tag \"shell\"? YES\n- Accept this job! \u2705<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 3: Runner claims job<\/p>\n<pre class=\"wp-block-code\"><code>POST https:\/\/gitlab.com\/api\/v4\/jobs\/12345\/claim\nHeaders:\n  Authorization: Bearer runner-token-xyz\n\nResponse:\n{\n  \"status\": \"claimed\",\n  \"runner_id\": 789\n}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 4: Runner executes job<\/p>\n<p class=\"wp-block-paragraph\">On macOS runner:<\/p>\n<pre class=\"wp-block-code\"><code># GitLab Runner executes these commands directly on macOS\ncd \/Users\/runner\/builds\/project\nmvn -v\nmvn clean package\nmvn test<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Why this matters:<\/p>\n<pre class=\"wp-block-code\"><code># Jenkins approach\npipeline {\n    agent { label 'macos-agent' }\n}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Single label, finds first matching agent.<\/p>\n<pre class=\"wp-block-code\"><code># GitLab approach\ntags:\n- macos\n- personal  \n- shell<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Must match ALL tags. More specific control.<\/p>\n<h3 class=\"wp-block-heading\">Deep Dive: Artifacts System<\/h3>\n<pre class=\"wp-block-code\"><code>artifacts:\n  name: war\n  paths:\n  - \"target\/my-webapp.war\"\n  reports:\n    junit:\n    - target\/surefire-reports\/TEST.*.xml\n  expire_in: 30 days<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What happens during artifact upload:<\/p>\n<p class=\"wp-block-paragraph\">Step 1: Job completes successfully<\/p>\n<p class=\"wp-block-paragraph\">GitLab Runner scans for artifacts:<\/p>\n<pre class=\"wp-block-code\"><code># Runner finds these files\n\/builds\/project\/target\/my-webapp.war\n\/builds\/project\/target\/surefire-reports\/TEST.UserServiceTest.xml\n\/builds\/project\/target\/surefire-reports\/TEST.AppTest.xml<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 2: Create ZIP archive<\/p>\n<pre class=\"wp-block-code\"><code># Runner creates temporary ZIP\ncd \/builds\/project\nzip -r \/tmp\/artifacts-12345.zip target\/my-webapp.war target\/surefire-reports\/TEST.*.xml\n\n# ZIP structure:\nartifacts-12345.zip\n\u251c\u2500\u2500 target\/\n\u2502   \u251c\u2500\u2500 my-webapp.war\n\u2502   \u2514\u2500\u2500 surefire-reports\/\n\u2502       \u251c\u2500\u2500 TEST.UserServiceTest.xml\n\u2502       \u2514\u2500\u2500 TEST.AppTest.xml<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 3: Upload to GitLab<\/p>\n<pre class=\"wp-block-code\"><code>POST https:\/\/gitlab.com\/api\/v4\/jobs\/12345\/artifacts\nHeaders:\n  Content-Type: multipart\/form-data\n  JOB-TOKEN: secret-job-token\nBody:\n  file: \/tmp\/artifacts-12345.zip\n  artifact_type: archive\n  expire_in: 2592000  (30 days in seconds)\n\nResponse:\n{\n  \"artifacts_file\": {\n    \"filename\": \"artifacts.zip\",\n    \"size\": 5242880  (5 MB)\n  },\n  \"artifacts_expire_at\": \"2026-01-19T10:30:00Z\"\n}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 4: Parse JUnit reports<\/p>\n<p class=\"wp-block-paragraph\">GitLab extracts and parses JUnit XML:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;!-- TEST.UserServiceTest.xml --&gt;\n&lt;testsuite tests=\"3\" failures=\"0\" errors=\"0\" time=\"0.532\"&gt;\n  &lt;testcase name=\"testUserCreation\" time=\"0.123\"\/&gt;\n  &lt;testcase name=\"testUserUpdate\" time=\"0.204\"\/&gt;\n  &lt;testcase name=\"testUserDeletion\" time=\"0.205\"\/&gt;\n&lt;\/testsuite&gt;<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Stores in database:<\/p>\n<pre class=\"wp-block-code\"><code>INSERT INTO ci_job_artifacts (\n  job_id,\n  file_type,\n  size,\n  expire_at,\n  file_store\n) VALUES (\n  12345,\n  'junit',\n  5242880,\n  '2026-01-19 10:30:00',\n  1  # local storage\n);<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 5: Display in UI<\/p>\n<p class=\"wp-block-paragraph\">GitLab UI shows:<\/p>\n<pre class=\"wp-block-code\"><code>Tests: 3 passed, 0 failed\nDuration: 0.532s\nCoverage: N\/A<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Artifact retrieval by other jobs:<\/p>\n<pre class=\"wp-block-code\"><code>deploy-job:\n  stage: deploy\n  dependencies:\n  - maven-test-job\n  script:\n  - ls target\/my-webapp.war  # File is here!<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What happens:<\/p>\n<ol class=\"wp-block-list\"><li>deploy-job starts<\/li><li>GitLab downloads artifacts-12345.zip<\/li><li>Extracts to job workspace<\/li><li>Job can access target\/my-webapp.war<\/li><\/ol>\n<p class=\"wp-block-paragraph\">Artifact expiration:<\/p>\n<p class=\"wp-block-paragraph\">After 30 days, GitLab cron job runs:<\/p>\n<pre class=\"wp-block-code\"><code># GitLab background job (simplified)\nArtifactExpireWorker.perform_async\n\ndef perform\n  expired = CiJobArtifact.where('expire_at &lt; ?', Time.now)\n  expired.find_each do |artifact|\n    artifact.file.remove!  # Delete from storage\n    artifact.destroy       # Delete database record\n  end\nend<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Deep Dive: SonarQube Integration<\/h3>\n<pre class=\"wp-block-code\"><code>sonarqube-check:\n  stage: sonarqube-stage\n  image: maven:3.6.3-jdk-11\n  variables:\n    SONAR_USER_HOME: \"${CI_PROJECT_DIR}\/.sonar\"\n    GIT_DEPTH: \"0\"\n  cache:\n    key: \"${CI_JOB_NAME}\"\n    paths:\n    - .sonar\/cache\n  script:\n  - mvn verify sonar:sonar -Dsonar.projectKey=root_test_AZOKVHhuL2aw2rzHZn9m\n  allow_failure: true\n  only:\n  - main<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Technical breakdown:<\/p>\n<p class=\"wp-block-paragraph\">1. Variable SONAR_USER_HOME:<\/p>\n<pre class=\"wp-block-code\"><code>SONAR_USER_HOME: \"${CI_PROJECT_DIR}\/.sonar\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What this does:<\/p>\n<p class=\"wp-block-paragraph\">Without this:<\/p>\n<pre class=\"wp-block-code\"><code>SonarQube stores cache in: \/root\/.sonar\/\nNext job runs: Fresh container, \/root\/.sonar\/ is empty\nResult: Re-downloads all analysis plugins (300MB)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">With this:<\/p>\n<pre class=\"wp-block-code\"><code>SonarQube stores cache in: \/builds\/project\/.sonar\/\nGitLab caches this directory\nNext job runs: Cache restored to \/builds\/project\/.sonar\/\nResult: Uses cached plugins (0MB download)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Time savings:<\/p>\n<ul class=\"wp-block-list\"><li>First run: 45 seconds to download SonarQube plugins<\/li><li>Cached run: 2 seconds to extract cache<\/li><li>43 seconds saved per run!<\/li><\/ul>\n<p class=\"wp-block-paragraph\">2. Variable GIT_DEPTH:<\/p>\n<pre class=\"wp-block-code\"><code>GIT_DEPTH: \"0\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Default GitLab behavior (GIT_DEPTH=50):<\/p>\n<pre class=\"wp-block-code\"><code>git clone --depth=50 https:\/\/gitlab.com\/user\/project.git<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Downloads only last 50 commits:<\/p>\n<pre class=\"wp-block-code\"><code>Commit history:\n[Latest] abc123 (downloaded)\n[Latest-1] def456 (downloaded)\n...\n[Latest-49] xyz789 (downloaded)\n[Latest-50] old999 (downloaded)\n[Latest-51] ancient (NOT downloaded)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Why this breaks SonarQube:<\/p>\n<p class=\"wp-block-paragraph\">SonarQube analyzes:<\/p>\n<ul class=\"wp-block-list\"><li>Code evolution over time<\/li><li>When bugs were introduced<\/li><li>Technical debt trends<\/li><\/ul>\n<p class=\"wp-block-paragraph\">Needs full Git history!<\/p>\n<p class=\"wp-block-paragraph\">With GIT_DEPTH=0:<\/p>\n<pre class=\"wp-block-code\"><code>git clone --depth=0 https:\/\/gitlab.com\/user\/project.git\n# Same as: git clone https:\/\/gitlab.com\/user\/project.git<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Downloads ENTIRE history:<\/p>\n<pre class=\"wp-block-code\"><code>All commits from first to latest\nSonarQube can analyze properly<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Trade-off:<\/p>\n<ul class=\"wp-block-list\"><li>GIT_DEPTH=50: Fast clone (5 seconds), incomplete analysis<\/li><li>GIT_DEPTH=0: Slow clone (30 seconds), complete analysis<\/li><\/ul>\n<p class=\"wp-block-paragraph\">3. Cache mechanism deep dive:<\/p>\n<pre class=\"wp-block-code\"><code>cache:\n  key: \"${CI_JOB_NAME}\"\n  paths:\n  - .sonar\/cache<\/code><\/pre>\n<p class=\"wp-block-paragraph\">How caching works:<\/p>\n<p class=\"wp-block-paragraph\">First run (cache miss):<\/p>\n<pre class=\"wp-block-code\"><code>Step 1: Job starts, no cache found\nStep 2: SonarQube downloads plugins to .sonar\/cache\/\nStep 3: Analysis completes\nStep 4: GitLab Runner creates cache archive:\n  tar -czf cache-sonarqube-check.tar.gz .sonar\/cache\/\nStep 5: Upload cache to GitLab server:\n  PUT \/api\/v4\/projects\/123\/cache?key=sonarqube-check\n  Body: cache-sonarqube-check.tar.gz (300MB)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Second run (cache hit):<\/p>\n<pre class=\"wp-block-code\"><code>Step 1: Job starts\nStep 2: Runner checks GitLab server:\n  GET \/api\/v4\/projects\/123\/cache?key=sonarqube-check\n  Response: cache-sonarqube-check.tar.gz exists\nStep 3: Download and extract:\n  curl -o cache.tar.gz https:\/\/gitlab.com\/...\/cache\n  tar -xzf cache.tar.gz\nStep 4: .sonar\/cache\/ directory restored\nStep 5: SonarQube uses cached plugins<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Cache key strategies:<\/p>\n<pre class=\"wp-block-code\"><code># Per-job caching\ncache:\n  key: \"${CI_JOB_NAME}\"  # Key: \"sonarqube-check\"\n\n# Per-branch caching\ncache:\n  key: \"${CI_COMMIT_REF_SLUG}\"  # Key: \"main\" or \"feature-123\"\n\n# Per-branch-per-job caching\ncache:\n  key: \"${CI_COMMIT_REF_SLUG}-${CI_JOB_NAME}\"  # Key: \"main-sonarqube-check\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">My choice: ${CI_JOB_NAME} because SonarQube cache is same across all branches.<\/p>\n<p class=\"wp-block-paragraph\">4. allow_failure behavior:<\/p>\n<pre class=\"wp-block-code\"><code>allow_failure: true<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What happens when job fails:<\/p>\n<p class=\"wp-block-paragraph\">Without allow_failure:<\/p>\n<pre class=\"wp-block-code\"><code>Job fails \u2192 Pipeline status: FAILED \u274c\nBlocks deployment\nManual intervention required<\/code><\/pre>\n<p class=\"wp-block-paragraph\">With allow_failure:<\/p>\n<pre class=\"wp-block-code\"><code>Job fails \u2192 Pipeline status: SUCCESS (with warnings) \u26a0\ufe0f\nDeployment continues\nQuality issues logged but don't block<\/code><\/pre>\n<p class=\"wp-block-paragraph\">My use case:<\/p>\n<p class=\"wp-block-paragraph\">SonarQube sometimes fails on:<\/p>\n<ul class=\"wp-block-list\"><li>Minor code smells<\/li><li>Slight decrease in coverage<\/li><li>Acceptable technical debt<\/li><\/ul>\n<p class=\"wp-block-paragraph\">I want to see the results but not block deployment.<\/p>\n<p class=\"wp-block-paragraph\">Job status combinations:<\/p>\n<pre class=\"wp-block-code\"><code>Job Result allow_failure Pipeline Status Success false Success \u2705 Success true Success \u2705 Failure false Failed \u274c Failure true Success (with warnings) \u26a0\ufe0f<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Container Registry: Technical Deep Dive<\/h3>\n<pre class=\"wp-block-code\"><code>docker-job:\n  stage: docker-build-stage\n  image: docker:20.10.16\n  services:\n  - docker:20.10.16-dind\n  before_script:\n  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD registry.gitlab.com\n  script:\n  - docker build -t registry.gitlab.com\/lowyiiii\/java-project .\n  - docker push registry.gitlab.com\/lowyiiii\/java-project<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Docker-in-Docker (DinD) Architecture<\/h3>\n<p class=\"wp-block-paragraph\">What actually happens when job runs:<\/p>\n<p class=\"wp-block-paragraph\">Step 1: Runner starts job container<\/p>\n<pre class=\"wp-block-code\"><code># GitLab Runner executes:\ndocker run \\\n  --name job-container \\\n  --network gitlab-runner \\\n  docker:20.10.16 \\\n  sh -c \"docker login ... &amp;&amp; docker build ...\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 2: Runner starts DinD service container<\/p>\n<pre class=\"wp-block-code\"><code># Simultaneously, Runner executes:\ndocker run \\\n  --name dind-service \\\n  --privileged \\\n  --network gitlab-runner \\\n  docker:20.10.16-dind<\/code><\/pre>\n<p class=\"wp-block-paragraph\">The &#8211;privileged flag is critical:<\/p>\n<p class=\"wp-block-paragraph\">Without &#8211;privileged:<\/p>\n<pre class=\"wp-block-code\"><code>  DinD container cannot access host kernel features\n  Cannot start Docker daemon\n  ERROR: Cannot connect to Docker daemon \u274c<\/code><\/pre>\n<p class=\"wp-block-paragraph\">With &#8211;privileged:<\/p>\n<pre class=\"wp-block-code\"><code>  DinD container gets full host access\n  Can start Docker daemon\n  Docker commands work \u2705<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 3: Containers communicate<\/p>\n<pre class=\"wp-block-code\"><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502   job-container         \u2502\n\u2502   (docker:20.10.16)     \u2502\n\u2502                         \u2502\n\u2502   $ docker build ...    \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n            \u2502\n            \u2502 TCP connection to\n            \u2502 docker:2376 (TLS)\n            \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502   dind-service          \u2502\n\u2502   (docker:20.10.16-dind)\u2502\n\u2502                         \u2502\n\u2502   Docker Daemon         \u2502\n\u2502   listening on :2376    \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Environment variables automatically set:<\/p>\n<pre class=\"wp-block-code\"><code># Inside job container:\necho $DOCKER_HOST\n# Output: tcp:\/\/docker:2376\n\necho $DOCKER_TLS_VERIFY\n# Output: 1\n\necho $DOCKER_CERT_PATH\n# Output: \/certs\/client<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This tells Docker client to connect to DinD daemon!<\/p>\n<p class=\"wp-block-paragraph\">Step 4: Docker build process<\/p>\n<pre class=\"wp-block-code\"><code># In job container:\ndocker build -t registry.gitlab.com\/lowyiiii\/java-project .\n\n# What happens:\n1. Docker client sends build context to DinD daemon\n2. DinD daemon builds image inside its own container\n3. Image stored in DinD daemon's storage\n4. Image layers never touch job container<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Storage architecture:<\/p>\n<pre class=\"wp-block-code\"><code>Host machine:\n\/var\/lib\/docker\/\n\u251c\u2500\u2500 containers\/\n\u2502   \u251c\u2500\u2500 dind-service\/\n\u2502   \u2514\u2500\u2500 job-container\/\n\u2514\u2500\u2500 volumes\/\n    \u2514\u2500\u2500 dind-storage\/\n        \u2514\u2500\u2500 overlay2\/\n            \u2514\u2500\u2500 [image layers stored here]<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 5: Docker push<\/p>\n<pre class=\"wp-block-code\"><code>docker push registry.gitlab.com\/lowyiiii\/java-project\n\n# Process:\n1. DinD daemon reads image layers from its storage\n2. Chunks layers into 10MB blobs\n3. Compresses with gzip\n4. Uploads to registry.gitlab.com via HTTPS\n5. Sends manifest listing all layers<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Network flow:<\/p>\n<pre class=\"wp-block-code\"><code>job-container \u2192 dind-service \u2192 internet \u2192 registry.gitlab.com\n   (client)      (daemon)                    (registry)<\/code><\/pre>\n<h3 class=\"wp-block-heading\">GitLab Container Registry Authentication<\/h3>\n<pre class=\"wp-block-code\"><code>before_script:\n- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD registry.gitlab.com<\/code><\/pre>\n<p class=\"wp-block-paragraph\">GitLab provides these variables automatically:<\/p>\n<pre class=\"wp-block-code\"><code># Job environment:\nCI_REGISTRY_USER=\"gitlab-ci-token\"\nCI_REGISTRY_PASSWORD=\"glcbt-Xyj4k9m...\"  # Job token (changes per job!)\nCI_REGISTRY=\"registry.gitlab.com\"\nCI_REGISTRY_IMAGE=\"registry.gitlab.com\/lowyiiii\/java-project\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">How job token authentication works:<\/p>\n<p class=\"wp-block-paragraph\">Step 1: Job starts, GitLab generates token<\/p>\n<pre class=\"wp-block-code\"><code># GitLab server (simplified)\ndef create_job_token(job)\n  token = SecureRandom.base64(32)\n  \n  JobToken.create!(\n    job_id: job.id,\n    token: token,\n    expires_at: job.finished_at + 1.hour\n  )\n  \n  token\nend<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 2: Docker login request<\/p>\n<pre class=\"wp-block-code\"><code>POST https:\/\/registry.gitlab.com\/jwt\/auth\nHeaders:\n  Authorization: Basic Z2l0bGFiLWNpLXRva2VuOmdsY2J0LVh5ajRrOW0uLi4=\n  # Base64(\"gitlab-ci-token:glcbt-Xyj4k9m...\")\n\nResponse:\n{\n  \"token\": \"eyJhbGciOiJSUzI1NiIs...\",  # JWT\n  \"expires_in\": 300,\n  \"issued_at\": \"2024-12-19T10:00:00Z\"\n}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 3: Docker saves token<\/p>\n<pre class=\"wp-block-code\"><code># Docker creates ~\/.docker\/config.json\n{\n  \"auths\": {\n    \"registry.gitlab.com\": {\n      \"auth\": \"Z2l0bGFiLWNpLXRva2VuOmdsY2J0LVh5ajRrOW0uLi4=\"\n    }\n  }\n}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 4: Docker push with JWT<\/p>\n<pre class=\"wp-block-code\"><code>PUT https:\/\/registry.gitlab.com\/v2\/lowyiiii\/java-project\/blobs\/uploads\/\nHeaders:\n  Authorization: Bearer eyJhbGciOiJSUzI1NiIs...\nBody:\n  [layer blob data]\n\nGitLab verifies JWT:\n1. Decodes JWT\n2. Checks signature\n3. Validates expiration\n4. Checks job permissions\n5. Allows upload \u2705<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Why job tokens are secure:<\/p>\n<p class=\"wp-block-paragraph\">Traditional password:<\/p>\n<ul class=\"wp-block-list\"><li>Same password for all jobs<\/li><li>If leaked, all projects compromised<\/li><li>Requires manual rotation<\/li><\/ul>\n<p class=\"wp-block-paragraph\">Job token:<\/p>\n<ul class=\"wp-block-list\"><li>Unique per job<\/li><li>Expires 1 hour after job completion<\/li><li>Automatically rotated<\/li><li>Limited to specific project<\/li><li>Job-specific permissions<\/li><\/ul>\n<h3 class=\"wp-block-heading\">AWS EKS Deployment: kubectl Installation Deep Dive<\/h3>\n<pre class=\"wp-block-code\"><code>deploy-job:\n  stage: deploy\n  image:\n    name: amazon\/aws-cli\n    entrypoint: [ \"\" ]\n  before_script:\n  - curl -O https:\/\/s3.us-west-2.amazonaws.com\/amazon-eks\/1.30.2\/2024-07-12\/bin\/linux\/amd64\/kubectl\n  - curl -O https:\/\/s3.us-west-2.amazonaws.com\/amazon-eks\/1.30.2\/2024-07-12\/bin\/linux\/amd64\/kubectl.sha256\n  - sha256sum -c kubectl.sha256\n  - chmod +x .\/kubectl\n  - mkdir -p $HOME\/bin &amp;&amp; cp .\/kubectl $HOME\/bin\/kubectl &amp;&amp; export PATH=$HOME\/bin:$PATH\n  script:\n  - aws configure set aws_access_key $AWS_ACCESS_KEY_ID\n  - kubectl config use-context \"$KUBE_CONTEXT\"\n  - kubectl apply -f Application.yaml<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Why Entrypoint Override is Necessary<\/h3>\n<p class=\"wp-block-paragraph\">Default amazon\/aws-cli Dockerfile:<\/p>\n<pre class=\"wp-block-code\"><code>FROM python:3.11-slim\nRUN pip install awscli\nENTRYPOINT [\"aws\"]\nCMD [\"--help\"]<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Without entrypoint override:<\/p>\n<pre class=\"wp-block-code\"><code>deploy-job:\n  image: amazon\/aws-cli\n  script:\n  - echo \"Starting deployment\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What GitLab Runner actually executes:<\/p>\n<pre class=\"wp-block-code\"><code>docker run amazon\/aws-cli sh -c \"echo 'Starting deployment'\"\n\n# Docker combines:\n# ENTRYPOINT [\"aws\"] + COMMAND [\"sh\", \"-c\", \"echo 'Starting deployment'\"]\n# Result: aws sh -c \"echo 'Starting deployment'\"\n# ERROR: Invalid AWS command! \u274c<\/code><\/pre>\n<p class=\"wp-block-paragraph\">With entrypoint override:<\/p>\n<pre class=\"wp-block-code\"><code>deploy-job:\n  image:\n    name: amazon\/aws-cli\n    entrypoint: [ \"\" ]\n  script:\n  - echo \"Starting deployment\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What GitLab Runner executes:<\/p>\n<pre class=\"wp-block-code\"><code>docker run --entrypoint=\"\" amazon\/aws-cli sh -c \"echo 'Starting deployment'\"\n\n# Docker uses:\n# ENTRYPOINT [] (empty)\n# COMMAND [\"sh\", \"-c\", \"echo 'Starting deployment'\"]\n# Result: sh -c \"echo 'Starting deployment'\"\n# SUCCESS: Starting deployment \u2705<\/code><\/pre>\n<h3 class=\"wp-block-heading\">kubectl Binary Verification Process<\/h3>\n<pre class=\"wp-block-code\"><code>curl -O https:\/\/s3.us-west-2.amazonaws.com\/amazon-eks\/1.30.2\/2024-07-12\/bin\/linux\/amd64\/kubectl\ncurl -O https:\/\/s3.us-west-2.amazonaws.com\/amazon-eks\/1.30.2\/2024-07-12\/bin\/linux\/amd64\/kubectl.sha256\nsha256sum -c kubectl.sha256<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What happens step-by-step:<\/p>\n<p class=\"wp-block-paragraph\">Step 1: Download kubectl binary<\/p>\n<pre class=\"wp-block-code\"><code>curl -O https:\/\/s3...\/kubectl\n\n# Downloads:\n-rwxr-xr-x 1 root root 48234567 Dec 19 10:00 kubectl<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 2: Download checksum file<\/p>\n<pre class=\"wp-block-code\"><code>curl -O https:\/\/s3...\/kubectl.sha256\n\n# Downloads:\ncat kubectl.sha256\n# Output:\n7d9e8c1f4e5a6b3c2d1a0f9e8d7c6b5a4e3d2c1b0a9f8e7d6c5b4a3e2d1c0f9e  kubectl<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 3: Verify checksum<\/p>\n<pre class=\"wp-block-code\"><code>sha256sum -c kubectl.sha256\n\n# sha256sum process:\n1. Reads kubectl.sha256 file\n2. Extracts expected hash: 7d9e8c1f4e5a6b3c...\n3. Calculates actual hash of kubectl binary:\n   sha256sum kubectl\n   # Calculated: 7d9e8c1f4e5a6b3c...\n4. Compares:\n   if expected == calculated:\n       print \"kubectl: OK\"\n       exit 0\n   else:\n       print \"kubectl: FAILED\"\n       exit 1<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Why this matters:<\/p>\n<p class=\"wp-block-paragraph\">Scenario 1: Compromised download<\/p>\n<pre class=\"wp-block-code\"><code># Attacker replaces kubectl with malicious version\n# Malicious kubectl hash: abc123...\n# AWS official hash: 7d9e8c...\n\nsha256sum -c kubectl.sha256\n# kubectl: FAILED\n# sha256sum: WARNING: 1 computed checksum did NOT match\n# Pipeline fails \u2705 Attack prevented!<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Scenario 2: Corrupted download<\/p>\n<pre class=\"wp-block-code\"><code># Network error causes partial download\n# Partial kubectl hash: def456...\n# Expected hash: 7d9e8c...\n\nsha256sum -c kubectl.sha256\n# kubectl: FAILED\n# Pipeline fails \u2705 Corruption detected!<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 4: Make kubectl executable and add to PATH<\/p>\n<pre class=\"wp-block-code\"><code>chmod +x .\/kubectl\n# Changes permissions:\n# Before: -rw-r--r-- kubectl\n# After:  -rwxr-xr-x kubectl\n\nmkdir -p $HOME\/bin\n# Creates: \/root\/bin\/\n\ncp .\/kubectl $HOME\/bin\/kubectl\n# Copies kubectl to: \/root\/bin\/kubectl\n\nexport PATH=$HOME\/bin:$PATH\n# Updates PATH:\n# Before: \/usr\/local\/bin:\/usr\/bin:\/bin\n# After:  \/root\/bin:\/usr\/local\/bin:\/usr\/bin:\/bin\n\n# Now kubectl is available:\nwhich kubectl\n# Output: \/root\/bin\/kubectl \u2705<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Kubernetes Context Management<\/h3>\n<pre class=\"wp-block-code\"><code>kubectl config use-context \"$KUBE_CONTEXT\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What is KUBE_CONTEXT?<\/p>\n<p class=\"wp-block-paragraph\">GitLab provides this variable from project settings:<\/p>\n<pre class=\"wp-block-code\"><code>Settings \u2192 CI\/CD \u2192 Variables:\nKUBE_CONTEXT = \"lowyiiii\/java-project:my-agent\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Kubeconfig structure:<\/p>\n<pre class=\"wp-block-code\"><code># ~\/.kube\/config\napiVersion: v1\nkind: Config\nclusters:\n- cluster:\n    certificate-authority-data: LS0tLS1CRUd...\n    server: https:\/\/abc123.gr7.us-east-1.eks.amazonaws.com\n  name: my-eks-cluster\ncontexts:\n- context:\n    cluster: my-eks-cluster\n    namespace: production\n    user: gitlab-deploy\n  name: lowyiiii\/java-project:my-agent\nusers:\n- name: gitlab-deploy\n  user:\n    token: k8s-aws-v1.aHR0cHM6...<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What kubectl config use-context does:<\/p>\n<pre class=\"wp-block-code\"><code>kubectl config use-context \"lowyiiii\/java-project:my-agent\"\n\n# Modifies ~\/.kube\/config:\ncurrent-context: lowyiiii\/java-project:my-agent\n\n# Now kubectl uses:\n# - Cluster: my-eks-cluster\n# - Namespace: production\n# - User: gitlab-deploy<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Verification:<\/p>\n<pre class=\"wp-block-code\"><code>kubectl config current-context\n# Output: lowyiiii\/java-project:my-agent\n\nkubectl cluster-info\n# Output:\n# Kubernetes control plane is running at https:\/\/abc123.gr7.us-east-1.eks.amazonaws.com\n# CoreDNS is running at https:\/\/abc123.gr7.us-east-1.eks.amazonaws.com\/api\/v1\/namespaces\/kube-system\/services\/kube-dns:dns\/proxy<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Built-in SAST: How It Works Internally<\/h3>\n<pre class=\"wp-block-code\"><code>sast:\n  stage: test\ninclude:\n- template: Jobs\/SAST.gitlab-ci.yml<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This ONE line enables security scanning!<\/p>\n<h3 class=\"wp-block-heading\">What Actually Happens<\/h3>\n<p class=\"wp-block-paragraph\">Step 1: Template inclusion<\/p>\n<pre class=\"wp-block-code\"><code>include:\n- template: Jobs\/SAST.gitlab-ci.yml<\/code><\/pre>\n<p class=\"wp-block-paragraph\">GitLab fetches from internal repository:<\/p>\n<pre class=\"wp-block-code\"><code>https:\/\/gitlab.com\/gitlab-org\/gitlab\/-\/blob\/master\/lib\/gitlab\/ci\/templates\/Jobs\/SAST.gitlab-ci.yml<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Template contents (simplified):<\/p>\n<pre class=\"wp-block-code\"><code># GitLab's SAST template\nvariables:\n  SAST_EXCLUDED_PATHS: \"spec,test,tests,tmp\"\n  SAST_EXCLUDED_ANALYZERS: \"\"\n\n.sast-analyzer:\n  stage: test\n  allow_failure: true\n  artifacts:\n    reports:\n      sast: gl-sast-report.json\n\nspotbugs-sast:\n  extends: .sast-analyzer\n  image: \"$SECURE_ANALYZERS_PREFIX\/spotbugs:$SAST_ANALYZER_IMAGE_TAG\"\n  rules:\n  - if: $SAST_DISABLED\n    when: never\n  - if: $CI_COMMIT_BRANCH\n    exists:\n    - \"**\/*.java\"\n    - \"**\/pom.xml\"\n\nsemgrep-sast:\n  extends: .sast-analyzer\n  image: \"$SECURE_ANALYZERS_PREFIX\/semgrep:$SAST_ANALYZER_IMAGE_TAG\"\n  rules:\n  - if: $SAST_DISABLED\n    when: never\n  - if: $CI_COMMIT_BRANCH<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 2: GitLab detects language<\/p>\n<pre class=\"wp-block-code\"><code># GitLab scans repository:\nfind . -name \"*.java\"\n# Found: src\/main\/java\/com\/example\/App.java\n\nfind . -name \"pom.xml\"\n# Found: pom.xml\n\n# Conclusion: Java project detected!\n# Runs: spotbugs-sast job<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 3: Spotbugs analyzer runs<\/p>\n<pre class=\"wp-block-code\"><code># GitLab starts container:\ndocker run \\\n  -v \/builds\/project:\/code \\\n  registry.gitlab.com\/gitlab-org\/security-products\/analyzers\/spotbugs:latest \\\n  \/analyzer run\n\n# Inside container:\ncd \/code\nmvn compile  # Compile Java code\nspotbugs -textui -xml -output gl-sast-report.json target\/classes\/\n\n# Spotbugs scans for:\n# - SQL injection\n# - XXE (XML External Entity)\n# - Path traversal\n# - Insecure randomness\n# - Hard-coded credentials<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 4: Generate SAST report<\/p>\n<pre class=\"wp-block-code\"><code>\/\/ gl-sast-report.json\n{\n  \"version\": \"14.0.0\",\n  \"vulnerabilities\": [\n    {\n      \"id\": \"SQL_INJECTION_HIBERNATE\",\n      \"category\": \"sast\",\n      \"name\": \"SQL Injection\",\n      \"message\": \"Potential SQL injection\",\n      \"description\": \"User input is used in SQL query without sanitization\",\n      \"severity\": \"High\",\n      \"scanner\": {\n        \"id\": \"spotbugs\",\n        \"name\": \"SpotBugs\"\n      },\n      \"location\": {\n        \"file\": \"src\/main\/java\/com\/example\/UserService.java\",\n        \"start_line\": 45,\n        \"end_line\": 47\n      },\n      \"identifiers\": [\n        {\n          \"type\": \"spotbugs_type\",\n          \"name\": \"SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE\",\n          \"value\": \"SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE\"\n        }\n      ]\n    }\n  ]\n}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 5: GitLab parses and displays<\/p>\n<pre class=\"wp-block-code\"><code>-- GitLab database\nINSERT INTO vulnerability_findings (\n  project_id,\n  scanner_id,\n  severity,\n  confidence,\n  report_type,\n  primary_identifier_id,\n  location_fingerprint,\n  raw_metadata\n) VALUES (\n  123,\n  456,\n  'high',\n  'medium',\n  'sast',\n  789,\n  'abc123...',\n  '{\"description\": \"Potential SQL injection\", ...}'\n);<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Step 6: UI display<\/p>\n<pre class=\"wp-block-code\"><code>Security Dashboard \u2192 SAST\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 1 High Severity Vulnerability Found       \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 SQL Injection                              \u2502\n\u2502 File: UserService.java:45                  \u2502\n\u2502 Status: Detected                           \u2502\n\u2502 [View Details] [Create Issue] [Dismiss]   \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Merge request integration:<\/p>\n<pre class=\"wp-block-code\"><code>Merge Request !123\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 \u26a0\ufe0f Security Report                         \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 SAST detected 1 new vulnerability:         \u2502\n\u2502 \u2022 SQL Injection (High) - UserService.java  \u2502\n\u2502                                            \u2502\n\u2502 [Block Merge] [Approve Override]          \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>","protected":false},"excerpt":{"rendered":"<p>Introduction: Why I Switched from Jenkins to GitLab After building Jenkins pipelines, I was comfortable with Jenkins. Then I discovered GitLab CI\/CD&hellip;<\/p>\n","protected":false},"author":2,"featured_media":90,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_rezaops_part":"1","_rezaops_linkedin_url":"https:\/\/www.linkedin.com\/pulse\/gitlab-cicd-container-native-pipelines-built-in-sast-reza-chegini-et2mc\/","footnotes":""},"categories":[27,10],"tags":[30,8,35,48,49,38],"series":[40],"class_list":["post-88","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ci-cd","category-devops","tag-ci-cd","tag-devops","tag-docker","tag-gitlab-ci-cd","tag-sast","tag-security","series-gitlab-ci-cd"],"jetpack_featured_media_url":"https:\/\/rezachegini.com\/wp-content\/uploads\/2026\/10\/gitlab-cicd-container-native-sast-cover.png","_links":{"self":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/88","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=88"}],"version-history":[{"count":1,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/88\/revisions"}],"predecessor-version":[{"id":89,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/88\/revisions\/89"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/media\/90"}],"wp:attachment":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=88"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=88"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=88"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=88"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}