Effective GitLab CI/CD workflow with multiple Terraform configurations? - continuous-integration

My team uses AWS for our infrastructure, across 3 different AWS accounts. We'll call them simply sandbox, staging, and production.
I recently set up Terraform against our AWS infrastructure, and its hierarchy maps against our accounts, then by either application, or AWS service itself. The repo structure looks something like this:
staging
iam
groups
main.tf
users
main.tf
s3
main.tf
sandbox
iam
...
production
applications
gitlab
main.tf
route53
main.tf
...
We're using separate configurations per AWS service (e.g., IAM or S3) or application (e.g., GitLab) so we don't end up with huge .tf files per account that would take a long time to apply updates for any one change. Ideally, we'd like to move away from the service-based configuration approach and move towards more application-based configurations, but the problem at hand remains the same either way.
This approach has been working fine when applying updates manually from the command line, but I'd love to move it to GitLab CI/CD to better automate our workflow, and that's where things have broken down.
In my existing setup, if I make an single change to, say, staging/s3/main.tf, GitLab doesn't seem to have a good way out of the box to only run terraform plan or terraform apply for that specific configuration.
If I instead moved everything into a single main.tf file for an entire AWS account (or multiple files but tied to a single state file), I could simply have GitLab trigger a job to do plan or apply to just that configuration. It might take 15 minutes to run based on the number of AWS resources we have in each account, but it's a potential option I suppose.
It seems like my issue might be ultimately related to how GitLab handles "monorepos" than how Terraform handles its workflow (after all, Terraform will happily plan/apply my changes if I simply tell it what has changed), although I'd also be interested in hearing about how people structure their Terraform environments given -- or in order to avoid entirely -- these limitations.
Has anyone solved an issue like this in their environment?

The nice thing about Terraform is that it's idempotent so you can just apply even if nothing has changed and it will be a no-op action anyway.
If for some reason you really only want to run a plan/apply on a specific directory when things change then you can achieve this by using only.changes so that Gitlab will only run the job if the specified files have changed.
So if you have your existing structure then it's as simple as doing something like this:
stages:
- terraform plan
- terraform apply
.terraform_template:
image: hashicorp/terraform:latest
before_script:
- LOCATION=$(echo ${CI_JOB_NAME} | cut -d":" -f2)
- cd ${LOCATION}
- terraform init
.terraform_plan_template:
stage: terraform plan
extends: .terraform_template
script:
- terraform plan -input=false -refresh=true -module-depth=-1 .
.terraform_apply_template:
stage: terraform apply
extends: .terraform_template
script:
- terraform apply -input=false -refresh=true -auto-approve=true .
terraform-plan:production/applications/gitlab:
extends: .terraform_plan_template
only:
refs:
- master
changes:
- production/applications/gitlab/*
- modules/gitlab/*
terraform-apply:production/applications/gitlab:
extends: .terraform_apply_template
only:
refs:
- master
changes:
- production/applications/gitlab/*
- modules/gitlab/*
I've also assumed the existence of modules that are in a shared location to indicate how this pattern can also look for changes elsewhere in the repo than just the directory you are running Terraform against.
If this isn't the case and you have a flatter structure and you're happy to have a single apply job then you can simplify this to something like:
stages:
- terraform
.terraform_template:
image: hashicorp/terraform:latest
stage: terraform
before_script:
- LOCATION=$(echo ${CI_JOB_NAME} | cut -d":" -f2)
- cd ${LOCATION}
- terraform init
script:
- terraform apply -input=false -refresh=true -auto-approve=true .
only:
refs:
- master
changes:
- ${CI_JOB_NAME}/*
production/applications/gitlab:
extends: .terraform_template
In general though this can just be avoided by allowing Terraform to run against all of the appropriate directories on every push (probably only applying on push to master or other appropriate branch) because, as mentioned, Terraform is idempotent so it won't do anything if nothing has changed. This also has the benefit that if your automation code hasn't changed but something has changed in your provider (such as someone opening up a security group) then Terraform will go put it back to how it should be the next time it is triggered.

Related

Monorepo in GitLab - Java/Spring:boot/MVN - CI/CD issue - How to run jobs in order

I'm a DevOps student and I have a project to run and I've stuck on something, I hope I can solve it with your help.
On our project we use Java monorepo which means we have multiple services in one repository, each in its own directory, six in total.
I have one main CI/CD scenario file .gitlab-ci.yml and dedicated scenario files for each microservice in its directory.
/
.gitlab-ci.yml
/EurecaServer/.gitlab-ci.yml
/ApiGateway/.gitlab-ci.yml
/UserService/.gitlab-ci.yml
/DepositService/.gitlab-ci.yml
/CreditService/.gitlab-ci.yml
/InfoService/.gitlab-ci.yml
In the root scenario file .gitlab-ci.yml I'm using 'include' to collect all microservices .gitlab-ci.yml files.
stages:
- docker-compose-start
- test
- build
- deploy
include:
- EurekaServer/.gitlab-ci.yml
- ApiGateway/.gitlab-ci.yml
- UserService/.gitlab-ci.yml
- DepositService/.gitlab-ci.yml
- CreditService/.gitlab-ci.yml
- InfoService/.gitlab-ci.yml
docker-compose-start:
stage: docker-compose-start
tags:
- shell-runner-1
script:
- docker-compose -f docker-compose.yml up --force-recreate -d
In those microservices scenario files I'm using 'needs' to make stages follow in order. First I have a test stage, then build and deploy at the end.
When the pipeline starts (from the root .gitlab-ci.yml), every microservice scenario runs randomly which fails in some stages build and deploy.
Is there a possibility to make separated microservices scenario files .gitlab-ci.yml runs in specific order? First - EurecaServer, second - ApiGateway, etc.
gitlab pipeline <--picture is here, sorry not enough reputation.
My fellow students advised me to try to make one job to build all six microservices and another to deploy all ms's but I'm not quite sure if it is the right way because I want to see every microservice job specifically to make troubleshooting more useful.
Since needs is already used to make the jobs follow the order of test, build, then deploy, stages are not needed for that.
Rather, stages can be used to specify the order to run microservices like so:
stages:
- docker-compose-start
- EurekaServer
- ApiGateway
And the jobs can include their stages like stage:ApiGateway.

Is there a way to work with multiple workspaces in parallel?

In a Continuous Integration setup (Jenkins) I'd like to deploy (terraform apply) changes of the same Terraform configuration code using multiple workspaces at the same time using the same copy of the repository - i.e. the very same directory with .tf files.
Stages are executed in parallel (on same node/agent) and each of them consists of the same sequence of operations (terraform workspace select and then terraform apply).
It doesn't seem supported as the attempts fail with lock errors, e.g.
# terraform workspace select "workspace1"
# terraform apply [...] -input=false -auto-approve
Switched to workspace "workspace1".
Acquiring state lock. This may take a few moments...
Error: Error locking state: Error acquiring the state lock: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: 52973611-a892-deac-985b-5aa28172fdaf
Path: my-project/env:/workspace5/state
Operation: OperationTypeApply
Who: #87ddd2118473
Version: 0.13.5
Created: 2021-01-11 15:20:06.635256155 +0000 UTC
Info:
So it looks like lock for workspace5 gets in the way for applying for workspace1.
Is there any way out?
I use:
Terraform 0.13.5
s3 as the backend
DynamoDB for locks
Just tested this locally so not sure if it will work or a CI system.
Rather than using terraform workspace select xxxx use the environment variable set for workspaces. So in one session do:
export TF_WORKSPACE=one-environment
terraform apply -input=false -auto-approve
Then in parralel you can do:
export TF_WORKSPACE=another-environment
terraform apply -input=false -auto-approve
This seems to work and doesnt throw any locking errors.
One idea I have (which seems to work) is to copy the whole codebase to a separate directory. I.e. each workspace to be applied to each own. And run terraform workspace select and terraform apply from separate copies.
It works. But it's lame (inefficient).
The only way I see you able to achieve what you want is by defining a lock resource in Jenkins and then essentially locking that job/step so it will only every execute it one at a time, or by ensuring that the jobs are ran on separate nodes/agents.

serverless remove lamda using gitlab CI

I'm using gitlab CI for deployment.
I'm running into a problem when the review branch is deleted.
stop_review:
variables:
GIT_STRATEGY: none
stage: cleanup
script:
- echo "$AWS_REGION"
- echo "Stopping review branch"
- serverless config credentials --provider aws --key ${AWS_ACCESS_KEY_ID} --secret ${AWS_SECRET_ACCESS_KEY}
- echo "$CI_COMMIT_REF_NAME"
- serverless remove --stage=$CI_COMMIT_REF_NAME --verbose
only:
- branches
except:
- master
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
when: manual
error is This command can only be run in a Serverless service directory. Make sure to reference a valid config file in the current working directory if you're using a custom config file
I have tried different GIT_STRATEGY, can some point me in right direction?
In order to run serverless remove, you'll need to have the serverless.yml file available, which means the actual repository will need to be cloned. (or that file needs to get to GitLab in some way).
It's required to have a serverless.yml configuration file available when you run serverless remove because the Serverless Framework allows users to provision infrastructure using not only the framework's YML configuration but also additional resources (like CloudFormation in AWS) which may or may not live outside of the specified app or stage CF Stack entirely.
In fact, you can also provision infrastructure into other providers as well (AWS, GCP, Azure, OpenWhisk, or actually any combination of these).
So it's not sufficient to simply identify the stage name when running sls remove, you'll need the full serverless.yml template.

Codebuild Workflow with environment variables

I have a monolith github project that has multiple different applications that I'd like to integrate with an AWS Codebuild CI/CD workflow. My issue is that if I make a change to one project, I don't want to update the other. Essentially, I want to create a logical fork that deploys differently based on the files changed in a particular commit.
Basically my project repository looks like this:
- API
-node_modules
-package.json
-dist
-src
- REACTAPP
-node_modules
-package.json
-dist
-src
- scripts
- 01_install.sh
- 02_prebuild.sh
- 03_build.sh
- .ebextensions
In terms of Deployment, my API project gets deployed to elastic beanstalk and my REACTAPP gets deployed as static files to S3. I've tried a few things but decided that the only viable approach is to manually perform this deploy step within my own 03_build.sh script - because there's no way to build this dynamically within Codebuild's Deploy step (I could be wrong).
Anyway, my issue is that I essentially need to create a decision tree to determine which project gets excecuted, so if I make a change to API and push, it doesn't automatically deploy REACTAPP to S3 unnecessarliy (and vica versa).
I managed to get this working on localhost by updating environment variables at certain points in the build process and then reading them in separate steps. However this fails on Codedeploy because of permission issues i.e. I don't seem to be able to update env variables from within the CI process itself.
Explicitly, my buildconf.yml looks like this:
version: 0.2
env:
variables:
VARIABLES: 'here'
AWS_ACCESS_KEY_ID: 'XXXX'
AWS_SECRET_ACCESS_KEY: 'XXXX'
AWS_REGION: 'eu-west-1'
AWS_BUCKET: 'mybucket'
phases:
install:
commands:
- sh ./scripts/01_install.sh
pre_build:
commands:
- sh ./scripts/02_prebuild.sh
build:
commands:
- sh ./scripts/03_build.sh
I'm running my own shell scripts to perform some logic and I'm trying to pass variables between scripts: install->prebuild->build
To give one example, here's the 01_install.sh where I diff each project version to determine whether it needs to be updated (excuse any minor errors in bash):
#!/bin/bash
# STAGE 1
# _______________________________________
# API PROJECT INSTALL
# Do if API version was changed in prepush (this is just a sample and I'll likely end up storing the version & previous version within the package.json):
if [[ diff ./api/version.json ./api/old_version.json ]] > /dev/null 2>&1
## then
echo "🤖 Installing dependencies in API folder..."
cd ./api/ && npm install
## Set a variable to be used by the 02_prebuild.sh script
TEST_API="true"
export TEST_API
else
echo "No change to API"
fi
# ______________________________________
# REACTAPP PROJECT INSTALL
# Do if REACTAPP version number has changed (similar to above):
...
Then in my next stage I read these variables to determine whether I should run tests on the project 02_prebuild.sh:
#!/bin/bash
# STAGE 2
# _________________________________
# API PROJECT PRE-BUILD
# Do if install was initiated
if [[ $TEST_API == "true" ]]; then
echo "🤖 Run tests on API project..."
cd ./api/ && npm run tests
echo $TEST_API
BUILD_API="true"
export BUILD_API
else
echo "Don't test API"
fi
# ________________________________
# TODO: Complete for REACTAPP, similar to above
...
In my final script I use the BUILD_API variable to build to the dist folder, then I deploy that to either Elastic Beanstalk (for API) or S3 (for REACTAPP).
When I run this locally it works, however when I run it on Codebuild I get a permissions failure presumably because my bash scripts cannot export ENV_VAR. I'm wondering either if anyone knows how to update ENV_VARIABLES from within the build process itself, or if anyone has a better approach to achieve my goals (conditional/ variable build process on Codebuild)
EDIT:
So an approach that I've managed to get working is instead of using Env variables, I'm creating new files with specific names using fs then reading the contents of the file to make logical decisions. I can access these files from each of the bash scripts so it works pretty elegantly with some automatic cleanup.
I won't edit the original question as it's still an issue and I'd like to know how/ if other people solved this. I'm still playing around with how to actually use the eb deploy and s3 cli commands within the build scripts as codebuild does not seem to come with the eb cli installed and my .ebextensions file does not seem to be honoured.
Source control repos like Github can be configured to send a post event to an API endpoint when you push to a branch. You can consume this post request in lambda through API Gateway. This event data includes which files were modified with the commit. The lambda function can then process this event to figure out what to deploy. If you’re struggling with deploying to your servers from the codebuild container, you might want to try posting an artifact to s3 with an installable package and then have your server grab it from there.

Gitlab-CI multi-project-pipeline

currently I'm trying to understand the Gitlab-CI multi-project-pipeline.
I want to achieve to run a pipeline if another pipeline has finshed.
Example:
I have one project nginx saved in namespace baseimages which contains some configuration like fast-cgi-params. The ci-file looks like this:
stages:
- release
- notify
variables:
DOCKER_HOST: "tcp://localhost:2375"
DOCKER_REGISTRY: "registry.mydomain.de"
SERVICE_NAME: "nginx"
DOCKER_DRIVER: "overlay2"
release:
stage: release
image: docker:git
services:
- docker:dind
script:
- docker build -t $SERVICE_NAME:latest .
- docker tag $SERVICE_NAME:latest $DOCKER_REGISTRY/$SERVICE_NAME:latest
- docker push $DOCKER_REGISTRY/$SERVICE_NAME:latest
only:
- master
notify:
stage: notify
image: appropriate/curl:latest
script:
- curl -X POST -F token=$CI_JOB_TOKEN -F ref=master https://gitlab.mydomain.de/api/v4/projects/1/trigger/pipeline
only:
- master
Now I want to have multiple projects to rely on this image and let them rebuild if my baseimage changes e.g. new nginx version.
baseimage
|
---------------------------
| | |
project1 project2 project3
If I add a trigger to the other project and insert the generated token at $GITLAB_CI_TOKEN the foreign pipeline starts but there is no combined graph as shown in the documentation (https://docs.gitlab.com/ee/ci/multi_project_pipelines.html)
How is it possible to show the full pipeline graph?
Do I have to add every project which relies on my baseimage to the CI-File of the baseimage or is it possible to subscribe the baseimage-pipline in each project?
The Multi-project pipelines is a paid for feature introduced in GitLab Premium 9.3, and can only be accessed using GitLab's Premium or Silver models.
A way to see this is to the right of the document title:
Well after some more digging into the documentation I found a little sentence which states that Gitlab CE provides features marked as Core-Feature.
We have 50+ Gitlab packages where this is needed. What we used to do was push a commit to a downstream package, wait for the CI to finish, then push another commit to the upstream package, wait for the CI to finish, etc. This was very time consuming.
The other thing you can do is manually trigger builds and you can manually determine the order.
If none of this works for you or you want a better way, I built a tool to help do this called Gitlab Pipes. I used it internally for many months and realized that people need something like this, so I did the work to make it public.
Basically it listens to Gitlab notifications and when it sees a commit to a package, it reads the .gitlab-pipes.yml file to determine that projects dependencies. It will be able to construct a dependency graph of your projects and build the consumer packages on downstream commits.
The documentation is here, it sort of tells you how it works. And then the primary app website is here.
If you click the versions history ... from multi_project_pipelines it reveals.
Made available in all tiers in GitLab 12.8.
Multi-project pipeline visualizations as of 13.10-pre is marked as premium however in my ee version the visualizations for down/upstream links are functional.
So reference Triggering a downstream pipeline using a bridge job
Before GitLab 11.8, it was necessary to implement a pipeline job that was responsible for making the API request to trigger a pipeline in a different project.
In GitLab 11.8, GitLab provides a new CI/CD configuration syntax to make this task easier, and avoid needing GitLab Runner for triggering cross-project pipelines. The following illustrates configuring a bridge job:
rspec:
stage: test
script: bundle exec rspec
staging:
variables:
ENVIRONMENT: staging
stage: deploy
trigger: my/deployment

Resources