Etairos.
⚡ InvestCloud Security Lakehouse

Automation Stack

Ansible, Jenkins, GitLab, and ACM Private CA working together. Every component managed as code, every change tracked in Git.

Automation Stack: Ansible + Jenkins + GitLab + CA

Author: RedEye Security | Date: 2026-04-06 | Status: Draft v1.0


Overview

Everything is code. No manual clicks in AWS console, no hand-edited configs, no snowflake servers.

Developer/Engineer
      │
      │  git push
      ▼
GitLab (source of truth)
      │
      │  webhook on push/MR
      ▼
Jenkins (CI/CD orchestrator)
      │
      ├── Terraform plan/apply (AWS infra)
      ├── Ansible playbooks (config + app deploy)
      ├── Helm charts (EKS workloads)
      └── Pipeline tests (lint, validate, security scan)

GitLab Repository Structure

gitlab.ic-internal.com/security/lakehouse/
├── terraform/
│   ├── modules/
│   │   ├── s3-lakehouse/
│   │   ├── kinesis-ingest/
│   │   ├── eks-cluster/
│   │   ├── athena-workgroup/
│   │   ├── glue-catalog/
│   │   ├── acm-pca/
│   │   └── iam-roles/
│   ├── environments/
│   │   ├── dev/
│   │   ├── staging/
│   │   └── prod/
│   └── global/          ← account-level: SCPs, IAM Identity Center
├── ansible/
│   ├── inventory/
│   │   ├── aws_ec2.yml  ← dynamic inventory (EC2 tags)
│   │   └── group_vars/
│   ├── playbooks/
│   │   ├── site.yml
│   │   ├── vector.yml
│   │   ├── grafana.yml
│   │   └── jenkins.yml
│   └── roles/
│       ├── vector/
│       ├── grafana/
│       ├── cert-manager/
│       └── common/
├── helm/
│   ├── vector/
│   ├── query-api/
│   ├── dashboard-gen/
│   └── ai-inference/
├── vector/
│   └── ocsf-transforms/   ← VRL files per sourcetype
├── iceberg/
│   └── schemas/
├── grafana/
│   └── dashboards/        ← JSON dashboard definitions
├── .gitlab-ci.yml
└── Jenkinsfile

Jenkins Pipeline Design

Pipeline 1: Terraform (infra changes)

// Jenkinsfile.terraform
pipeline {
  agent { label 'terraform' }
  stages {
    stage('Checkout') { steps { checkout scm } }
    stage('TF Lint') {
      steps { sh 'tflint --recursive' }
    }
    stage('TF Plan') {
      steps {
        withCredentials([aws(credentialsId: 'aws-terraform-role')]) {
          sh 'terraform -chdir=terraform/environments/${ENV} plan -out=tfplan'
        }
      }
    }
    stage('Approval') {
      when { branch 'main' }
      steps { input 'Apply to ${ENV}?' }
    }
    stage('TF Apply') {
      when { branch 'main' }
      steps {
        withCredentials([aws(credentialsId: 'aws-terraform-role')]) {
          sh 'terraform -chdir=terraform/environments/${ENV} apply tfplan'
        }
      }
    }
  }
  post {
    always {
      archiveArtifacts 'terraform/environments/${ENV}/tfplan'
      slackSend channel: '#security-lakehouse-deploys', message: "TF ${currentBuild.result}: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
    }
  }
}

Pipeline 2: Ansible (config deploy)

pipeline {
  agent { label 'ansible' }
  stages {
    stage('Lint') {
      steps { sh 'ansible-lint ansible/playbooks/' }
    }
    stage('Syntax Check') {
      steps { sh 'ansible-playbook ansible/playbooks/site.yml --syntax-check' }
    }
    stage('Deploy') {
      steps {
        withCredentials([
          string(credentialsId: 'ansible-vault-pass', variable: 'VAULT_PASS'),
          sshUserPrivateKey(credentialsId: 'ansible-ssh-key', keyFileVariable: 'SSH_KEY')
        ]) {
          sh '''
            echo $VAULT_PASS > .vault-pass
            ansible-playbook ansible/playbooks/site.yml \
              -i ansible/inventory/aws_ec2.yml \
              --vault-password-file .vault-pass \
              --private-key $SSH_KEY \
              --limit ${TARGET_HOSTS:-all}
          '''
        }
      }
    }
  }
}

Pipeline 3: Helm / EKS workloads

pipeline {
  agent { label 'helm' }
  stages {
    stage('Lint') { steps { sh 'helm lint helm/${APP}/' } }
    stage('Template Check') {
      steps { sh 'helm template helm/${APP}/ | kubeval --strict' }
    }
    stage('Deploy') {
      steps {
        withCredentials([kubeconfigFile(credentialsId: 'eks-kubeconfig', variable: 'KUBECONFIG')]) {
          sh '''
            helm upgrade --install ${APP} helm/${APP}/ \
              --namespace ${NAMESPACE} \
              --values helm/${APP}/values-${ENV}.yaml \
              --set image.tag=${GIT_COMMIT:0:7} \
              --wait --timeout 5m
          '''
        }
      }
    }
  }
}

Pipeline 4: Dashboard Deploy (AI-generated)

pipeline {
  agent { label 'python' }
  stages {
    stage('Generate') {
      steps {
        sh '''
          python scripts/generate_dashboard.py \
            --ticket-id ${JIRA_TICKET} \
            --output grafana/dashboards/auto/${JIRA_TICKET}.json
        '''
      }
    }
    stage('Validate') {
      steps { sh 'python scripts/validate_dashboard.py grafana/dashboards/auto/${JIRA_TICKET}.json' }
    }
    stage('PR') {
      steps {
        sh '''
          git checkout -b auto/dashboard-${JIRA_TICKET}
          git add grafana/dashboards/auto/
          git commit -m "feat: auto-generate dashboard for ${JIRA_TICKET}"
          git push origin auto/dashboard-${JIRA_TICKET}
          glab mr create --title "Auto: Dashboard for ${JIRA_TICKET}" --fill
        '''
      }
    }
    stage('Deploy on Merge') {
      when { branch 'main' }
      steps {
        sh 'python scripts/deploy_dashboard.py grafana/dashboards/auto/${JIRA_TICKET}.json'
      }
    }
  }
}

GitLab CI (.gitlab-ci.yml)

stages:
  - validate
  - plan
  - deploy
  - notify

variables:
  TF_ROOT: terraform/environments/${CI_ENVIRONMENT_NAME}
  AWS_DEFAULT_REGION: us-east-1

.aws_auth: &aws_auth
  before_script:
    - aws sts assume-role --role-arn $TF_ROLE_ARN --role-session-name gitlab-ci > /tmp/creds.json
    - export AWS_ACCESS_KEY_ID=$(jq -r .Credentials.AccessKeyId /tmp/creds.json)
    - export AWS_SECRET_ACCESS_KEY=$(jq -r .Credentials.SecretAccessKey /tmp/creds.json)
    - export AWS_SESSION_TOKEN=$(jq -r .Credentials.SessionToken /tmp/creds.json)

tf:validate:
  stage: validate
  image: hashicorp/terraform:1.7
  script:
    - cd $TF_ROOT && terraform init -backend=false && terraform validate
  rules:
    - changes: ["terraform/**/*"]

tf:plan:
  stage: plan
  image: hashicorp/terraform:1.7
  <<: *aws_auth
  script:
    - cd $TF_ROOT && terraform init && terraform plan -out=tfplan
  artifacts:
    paths: [$TF_ROOT/tfplan]
  environment: staging
  rules:
    - if: $CI_MERGE_REQUEST_IID

tf:apply:
  stage: deploy
  image: hashicorp/terraform:1.7
  <<: *aws_auth
  script:
    - cd $TF_ROOT && terraform apply tfplan
  environment: production
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

ansible:deploy:
  stage: deploy
  image: cytopia/ansible:latest
  script:
    - ansible-playbook ansible/playbooks/site.yml
      -i ansible/inventory/aws_ec2.yml
      --vault-password-file <(echo $ANSIBLE_VAULT_PASS)
  rules:
    - changes: ["ansible/**/*"]
    - if: $CI_COMMIT_BRANCH == "main"

Ansible Role: Vector (OCSF ingest)

# roles/vector/tasks/main.yml
- name: Install Vector
  apt:
    deb: "https://apt.vector.dev/pool/v/vector/vector_0.38.0-1_amd64.deb"

- name: Deploy Vector config
  template:
    src: vector.yaml.j2
    dest: /etc/vector/vector.yaml
    owner: vector
    mode: '0640'
  notify: restart vector

- name: Request TLS cert from ACM PCA
  command: >
    aws acm-pca issue-certificate
    --certificate-authority-arn {{ acm_pca_arn }}
    --csr file://{{ vector_csr_path }}
    --signing-algorithm SHA256WITHRSA
    --validity Value=365,Type=DAYS
  register: cert_arn

- name: Ensure Vector running
  systemd:
    name: vector
    state: started
    enabled: yes

Ansible Role: cert-manager (EKS)

# roles/cert-manager/tasks/main.yml
- name: Install cert-manager via Helm
  kubernetes.core.helm:
    name: cert-manager
    chart_ref: jetstack/cert-manager
    namespace: cert-manager
    create_namespace: true
    values:
      installCRDs: true
      serviceAccount:
        annotations:
          eks.amazonaws.com/role-arn: "{{ cert_manager_irsa_role }}"

- name: Deploy ACM PCA ClusterIssuer
  kubernetes.core.k8s:
    definition:
      apiVersion: awspca.cert-manager.io/v1beta1
      kind: AWSPCAClusterIssuer
      metadata:
        name: investcloud-pca
      spec:
        arn: "{{ acm_pca_arn }}"
        region: "{{ aws_region }}"

Certificate Lifecycle

ACM Private CA (Subordinate to Investcloud CA)
      │
      │  cert-manager + aws-privateca-issuer
      ▼
EKS pod certs (24-hour TTL, auto-renew at 12h)
      │
      ├── Vector pods: client cert for mTLS to Kinesis
      ├── Query API pods: server cert for HTTPS
      ├── AI inference pods: server cert
      └── Grafana: server cert (via ALB + ACM public cert for external)

Cert rotation: automatic, zero-touch
Monitoring: cert-manager Prometheus metrics → Grafana alert if cert < 6h from expiry

Secrets Management

AWS Secrets Manager
├── /lakehouse/minio/root-creds
├── /lakehouse/kinesis/access-key
├── /lakehouse/grafana/admin-password
├── /lakehouse/jira/api-token
├── /lakehouse/zendesk/api-token
└── /lakehouse/llm/bedrock-config

EKS: External Secrets Operator
  - Syncs Secrets Manager → Kubernetes Secrets
  - Rotation: automatic (Secrets Manager rotates, ESO syncs within 1min)

Jenkins: credentials store
  - AWS role ARNs (assumed via IAM Identity Center)
  - GitLab API token (for MR creation in dashboard pipeline)
  - Ansible vault password

Monitoring the Automation Stack Itself

What Tool Alert condition
Pipeline failures Jenkins + Slack Any stage fails
Terraform drift Scheduled TF plan Plan shows changes not in Git
Ansible idempotency Weekly full run Any task changes
Cert expiry Grafana alert < 6h remaining
Vector ingest lag Kinesis CloudWatch ConsumerLag > 10k records
EKS pod health kube-state-metrics Pod CrashLoopBackOff