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 |