Skip to content

Deploy changed CloudFormation templates #1576

Deploy changed CloudFormation templates

Deploy changed CloudFormation templates #1576

name: Deploy changed CloudFormation templates
on:
workflow_dispatch:
inputs:
environment_name:
type: string
description: Select the environment name to run the actions on
required: true
default: all
environment_type:
type: choice
description: Select environment type
options:
- staging
- production
- staging and production
default: staging
push:
branches:
- master
paths:
- 'cf/**.yaml'
- 'cf/**.yml'
pull_request:
branches:
- master
paths:
- 'cf/**.yaml'
- 'cf/**.yml'
permissions:
id-token: write
contents: read
jobs:
load-config:
uses: ./.github/workflows/load-config.yaml
with:
environment_name: ${{ github.event.inputs.environment_name }}
environment_type: ${{ github.event.inputs.environment_type }}
check-secrets:
name: Check secrets
needs: load-config
runs-on: ubuntu-20.04
if: github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch'
strategy:
matrix:
environment_name: ${{fromJson(needs.load-config.outputs.environment_names)}}
environment: ${{ matrix.environment_name }}
steps:
- id: check-secrets
name: Check if necessary secrets are installed.
run: |-
if [ -z "${{ secrets.AWS_ACCOUNT_ID }}" ]
then
echo "This workflow requires AWS_ACCOUNT_ID defined in this repository secrets to complete."
echo "The secrets can be set/rotated by running 'rotate-ci' from 'cellenics-utils'."
ERROR=true
fi
if [ ! -z "$ERROR" ]
then
echo
echo This workflow requires some secrets to complete.
echo Please make they are created by adding/rotating them manually.
exit 1
fi
get-modified-templates:
name: Fetch paths to modified CloudFormation templates
runs-on: ubuntu-20.04
needs: load-config
outputs:
files: ${{ steps.exclude-services.outputs.files }}
num-files: ${{ steps.exclude-services.outputs.num-files }}
steps:
- id: checkout
name: Check out current branch source code
uses: actions/checkout@v3
with:
fetch-depth: 0
- id: validate-workflow-dispatch
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master'
name: Validate workflow dispatch
run: |-
echo Deploying the master branch via workflow_dispatch is not supported.
echo To deploy master branch, raise a PR with changes to the CF files
echo that needs to be deployed and merge the PR.
exit 1
- id: get-changed-files-on-pr-merge
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
name: Get changed files on PR merge to master
uses: lots0logs/[email protected]
with:
token: ${{ secrets.GITHUB_TOKEN }}
- id: get-changed-files-on-branch
if: github.ref != 'refs/heads/master'
name: Get changed files on branch
run: |
echo "[]" > empty.json
# Get name of changed files
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT remotes/origin/master ${{ github.sha }})
# If there are no changed files, create an empty array
# else concatenate into an array
if [ -z "$CHANGED_FILES" ]; then
echo "[]" > ${HOME}/files.json
else
echo "$CHANGED_FILES" | xargs -I 'value' jq --arg filename "value" '. |= . + [$filename]' empty.json | jq -s 'add' > ${HOME}/files.json
fi
echo "All changed files"
cat ${HOME}/files.json
- id: check-number-of-cf-files
name: Check CloudFormation templates
run: |-
# Select those that are CF templates (path starts with `cf/`)
jq '[.[] | select(match("^cf/"))]' ${HOME}/files.json > ${HOME}/changed_cf_files.json
- id: exclude-services
name: Exclude services
run: |-
echo "Excluding services: $EXCLUDED_SERVICES"
# Select files that are not in excluded_services
jq --argjson excluded_services "$EXCLUDED_SERVICES" 'map(select(. as $in | $excluded_services | any(. == $in) | not))' ${HOME}/changed_cf_files.json > ${HOME}/filtered_cf_files.json
# Set as output the minified JSON.
echo "Filtered CF files"
cat ${HOME}/filtered_cf_files.json
echo "num-files=$(jq '. | length' ${HOME}/filtered_cf_files.json)" >> $GITHUB_OUTPUT
echo "files=$(jq -c . < ${HOME}/filtered_cf_files.json)" >> $GITHUB_OUTPUT
env:
EXCLUDED_SERVICES: ${{ needs.load-config.outputs.excluded_services }}
lint-templates:
name: Lint template files
runs-on: ubuntu-20.04
needs: get-modified-templates
if: needs.get-modified-templates.outputs.num-files > 0
strategy:
matrix:
template: ${{fromJson(needs.get-modified-templates.outputs.files)}}
steps:
- id: checkout
name: Check out source code
uses: actions/checkout@v3
- id: lint
name: Lint template
uses: scottbrenner/[email protected]
with:
args: ${{ matrix.template }}
deploy-templates:
name: Deploy changed CloudFormation template
runs-on: ubuntu-20.04
needs: [load-config, check-secrets, get-modified-templates, lint-templates]
if: github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch'
outputs:
deploy-rds: ${{ steps.set-name.outputs.deploy-rds }}
strategy:
max-parallel: 1
matrix:
environment: ${{fromJson(needs.load-config.outputs.deployment_matrix)}}
template: ${{fromJson(needs.get-modified-templates.outputs.files)}}
environment: ${{ matrix.environment.name }}
env:
CLUSTER_ENV: ${{ matrix.environment.type }}
TEMPLATES_WITH_EXTRA_PARAMS: ("cf/sns.yaml" "cf/ses.yaml" "cf/post-register-lambda.yaml", "cf/canaries.yaml", "cf/cognito-case-insensitive.yaml", "cf/batch-job-definition.yaml")
steps:
- id: checkout
name: Check out source code
uses: actions/checkout@v3
# - name: Get parameters
# id: get-params
# run: echo "::set-output name=params::$(./get-params-for-template.sh ${{ matrix.template }})"
# shell: bash
- id: set-up-creds
name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/ci-iac-role
aws-region: ${{ secrets.AWS_REGION }}
- id: set-name
name: Set name of the CloudFormation stack
run: |-
echo "Ref is $GITHUB_REF"
BASE_NAME=$(basename $FILE_NAME | sed "s/\..*//")
STACK_NAME=biomage-$BASE_NAME-$CLUSTER_ENV
echo "stack-name=$STACK_NAME" >> $GITHUB_OUTPUT
if [ "$BASE_NAME" == 'rds' ]; then
echo "deploy-rds=true" >> $GITHUB_OUTPUT
fi
env:
FILE_NAME: ${{ matrix.template }}
- id: using-self-signed-certificate
name: Get config for whether deployment is using self-signed certificate
uses: mikefarah/yq@master
with:
cmd: yq '.[env(ENVIRONMENT_NAME)].selfSignedCertificate' 'infra/config/github-environments-config.yaml'
env:
ENVIRONMENT_NAME: ${{ matrix.environment.name }}
## TODO - GET THE "parameter-overrides" dynamically, to use one deploy action for all templates
- id: deploy-template
name: Deploy CloudFormation template
if: ${{ !contains(matrix.template, 'irsa-') && !contains(env.TEMPLATES_WITH_EXTRA_PARAMS, matrix.template) }}
uses: aws-actions/aws-cloudformation-github-deploy@v1
with:
parameter-overrides: "Environment=${{ matrix.environment.type }}"
name: ${{ steps.set-name.outputs.stack-name }}
template: ${{ matrix.template }}
no-fail-on-empty-changeset: "1"
capabilities: "CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND"
- id: deploy-canaries-template
name: Deploy CloudFormation PostRegisterLambda template
if: ${{ matrix.template == 'cf/canaries.yaml' }}
uses: aws-actions/aws-cloudformation-github-deploy@v1
with:
parameter-overrides: "Environment=${{ matrix.environment.type }},SupportEmail=${{ needs.load-config.outputs.support_email }}"
name: ${{ steps.set-name.outputs.stack-name }}
template: ${{ matrix.template }}
no-fail-on-empty-changeset: "1"
capabilities: "CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND"
- id: deploy-batch-job-definition-template
name: Deploy CloudFormation batch-job-definition template
if: ${{ matrix.template == 'cf/batch-job-definition.yaml' }}
uses: aws-actions/aws-cloudformation-github-deploy@v1
with:
parameter-overrides: >-
Environment=${{ matrix.environment.type }},
ImageAccountId=${{ needs.load-config.outputs.image_account_id }},
ImageAccountRegion=${{ needs.load-config.outputs.image_account_region }}
name: ${{ steps.set-name.outputs.stack-name }}
template: ${{ matrix.template }}
no-fail-on-empty-changeset: "1"
capabilities: "CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND"
- id: deploy-cognito-case-insensitive
name: Deploy CloudFormation cognito-case-insensitive template
if: ${{ matrix.template == 'cf/cognito-case-insensitive.yaml' }}
uses: aws-actions/aws-cloudformation-github-deploy@v1
with:
parameter-overrides: >-
Environment=${{ matrix.environment.type }},
ReplyEmail=${{ needs.load-config.outputs.reply_email }},
UserPoolDomainName=${{ needs.load-config.outputs.user_pool_domain_name }}
name: ${{ steps.set-name.outputs.stack-name }}
template: ${{ matrix.template }}
no-fail-on-empty-changeset: "1"
capabilities: "CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND"
- id: deploy-post-register-lambda-template
name: Deploy CloudFormation PostRegisterLambda template
if: ${{ matrix.template == 'cf/post-register-lambda.yaml' }}
uses: aws-actions/aws-cloudformation-github-deploy@v1
with:
parameter-overrides: "Environment=${{ matrix.environment.type }},UsingSelfSignedCert=${{ steps.using-self-signed-certificate.outputs.result }}"
name: ${{ steps.set-name.outputs.stack-name }}
template: ${{ matrix.template }}
no-fail-on-empty-changeset: "1"
capabilities: "CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND"
- id: deploy-sns-template
name: Deploy CloudFormation SNS template
if: ${{ matrix.template == 'cf/sns.yaml' }}
uses: aws-actions/aws-cloudformation-github-deploy@v1
with:
parameter-overrides: "Environment=${{ matrix.environment.type }},UsingSelfSignedCert=${{ steps.using-self-signed-certificate.outputs.result }}"
name: ${{ steps.set-name.outputs.stack-name }}
template: ${{ matrix.template }}
no-fail-on-empty-changeset: "1"
capabilities: "CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND"
- id: deploy-ses-template
name: Deploy CloudFormation SES template
if: ${{ matrix.template == 'cf/ses.yaml' && needs.load-config.outputs.should_deploy_ses == 'true' }}
uses: aws-actions/aws-cloudformation-github-deploy@v1
with:
parameter-overrides: "Environment=${{ matrix.environment.type }},DomainName=${{ secrets.DOMAIN_NAME }},PrimaryDomainName=${{ secrets.PRIMARY_DOMAIN_NAME }}"
name: ${{ steps.set-name.outputs.stack-name }}
template: ${{ matrix.template }}
no-fail-on-empty-changeset: "1"
# The following steps are only necessary for IAM Service Account roles.
- id: get-oidc
if: ${{ contains(matrix.template, 'irsa-') }}
name: Get OIDC provider information for IRSA role
run: |-
OIDC_PROVIDER=$(aws eks describe-cluster --name "biomage-$CLUSTER_ENV" --query "cluster.identity.oidc.issuer" --output text | sed -e "s/^https:\/\///")
echo "oidc-provider=$OIDC_PROVIDER" >> $GITHUB_OUTPUT
- id: deploy-irsa-template
if: ${{ contains(matrix.template, 'irsa-') }}
name: Deploy IRSA CloudFormation template
uses: aws-actions/aws-cloudformation-github-deploy@v1
with:
parameter-overrides: "Environment=${{ matrix.environment.type }},OIDCProvider=${{ steps.get-oidc.outputs.oidc-provider }}"
name: ${{ steps.set-name.outputs.stack-name }}
template: ${{ matrix.template }}
no-fail-on-empty-changeset: "1"
capabilities: "CAPABILITY_IAM,CAPABILITY_NAMED_IAM"
setup-rds-roles:
name: Setup RDS roles for default resources
runs-on: ubuntu-20.04
needs: [check-secrets, deploy-templates, load-config]
if: ${{ needs.deploy-templates.outputs.deploy-rds == 'true' }}
strategy:
max-parallel: 1
matrix:
environment: ${{ fromJson(needs.load-config.outputs.deployment_matrix) }}
environment: ${{ matrix.environment.name }}
env:
CLUSTER_ENV: ${{ matrix.environment.type }}
steps:
- id: set-up-creds
name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/ci-iac-role
aws-region: ${{ secrets.AWS_REGION }}
# This step is needed to change the environment name (e.g. staging) into its uppercase form.
# This is required because the action that is used in get-rds-secrets store the secret in environment variables,
# with uppercase letters (see action ref). Therefore, we need the uppercased form of the environment
# Which is output in .outputs.uppercase of this step.
- id: setup-rds-env
name: Uppercase environment name
uses: ASzc/change-string-case-action@v2
with:
string: ${{ matrix.environment.type }}
- id: get-rds-secrets
name: Export RDS secrets into environment variables
uses: abhilash1in/[email protected]
with:
secrets: aurora-${{ matrix.environment.type }}
parse-json: true
- id: check-rds-secrets
name: Check RDS secrets are fetched
run: |-
# These checks if the RDS username and passwords are set
if [ -z $AURORA_${{ steps.setup-rds-env.outputs.uppercase }}_USERNAME ]; then
echo "RDS username not provided"
exit 1
fi
if [ -z $AURORA_${{ steps.setup-rds-env.outputs.uppercase }}_PASSWORD ]; then
echo "RDS password not provided"
exit 1
fi
- id: setup-rds-roles
name: Setup RDS roles for default RDS instances
run: |-
INSTANCE_ID=$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=rds-${CLUSTER_ENV}-ssm-agent" \
--output text \
--query 'Reservations[*].Instances[*].InstanceId')
if [ -z $INSTANCE_ID ]; then
echo "Can not connect to RDS agent: No instances found for $CLUSTER_ENV"
exit 1
fi
CLUSTER_NAME=aurora-cluster-${CLUSTER_ENV}-default
RDSHOST=$(aws rds describe-db-cluster-endpoints \
--region $REGION \
--db-cluster-identifier $CLUSTER_NAME \
--filter Name=db-cluster-endpoint-type,Values='writer' \
--query 'DBClusterEndpoints[0].Endpoint' \
--output text)
if [ -z $RDSHOST ]; then
echo "Failed getting RDS host with name $CLUSTER_NAME"
exit 1
fi
ENSURE_PSQL_INSTALLED_COMMAND="sudo yum -y install postgresql"
aws ssm send-command --instance-ids "$INSTANCE_ID" \
--document-name AWS-RunShellScript \
--parameters "commands='$ENSURE_PSQL_INSTALLED_COMMAND'"
SETUP_ROLES_CMD="
PGPASSWORD=\'${AURORA_${{ steps.setup-rds-env.outputs.uppercase }}_PASSWORD}\' psql \
--host=${RDSHOST} \
--port=5432 \
--username=${AURORA_${{ steps.setup-rds-env.outputs.uppercase }}_USERNAME} \
--dbname=aurora_db <<EOF
CREATE ROLE api_role WITH LOGIN;
CREATE ROLE dev_role WITH LOGIN;
GRANT USAGE ON SCHEMA public TO api_role;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public to api_role;
GRANT dev_role TO ${AURORA_${{ steps.setup-rds-env.outputs.uppercase }}_USERNAME};
ALTER DEFAULT PRIVILEGES FOR USER dev_role IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO api_role;
ALTER DEFAULT PRIVILEGES FOR USER dev_role IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO api_role;
REVOKE dev_role FROM ${AURORA_${{ steps.setup-rds-env.outputs.uppercase }}_USERNAME};
GRANT rds_iam TO api_role;
GRANT rds_iam, ${AURORA_${{ steps.setup-rds-env.outputs.uppercase }}_USERNAME} TO dev_role;
EOF"
aws ssm send-command --instance-ids "$INSTANCE_ID" \
--document-name AWS-RunShellScript \
--parameters "commands='$SETUP_ROLES_CMD'"
env:
REGION: ${{ secrets.AWS_REGION }}
- id: install-crowdstrike-on-rds
name: Install CrowdStrike Sensor for default RDS instances
run: |-
if [[ -n "${{ secrets.FALCON_CID }}" ]];
then
INSTANCE_ID=$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=rds-${CLUSTER_ENV}-ssm-agent" \
--output text \
--query 'Reservations[*].Instances[*].InstanceId')
if [ -z $INSTANCE_ID ]; then
echo "Can not connect to RDS agent: No instances found for $CLUSTER_ENV"
exit 1
fi
CLUSTER_NAME=aurora-cluster-${CLUSTER_ENV}-default
RDSHOST=$(aws rds describe-db-cluster-endpoints \
--region $REGION \
--db-cluster-identifier $CLUSTER_NAME \
--filter Name=db-cluster-endpoint-type,Values='writer' \
--query 'DBClusterEndpoints[0].Endpoint' \
--output text)
if [ -z $RDSHOST ]; then
echo "Failed getting RDS host with name $CLUSTER_NAME"
exit 1
fi
INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.6.0/bash/install/falcon-linux-install.sh"
INSTALL_FALCON_COMMAND="
export FALCON_CLIENT_ID=${{ secrets.FALCON_CLIENT_ID }} && \
export FALCON_CLIENT_SECRET=${{ secrets.FALCON_CLIENT_SECRET }} && \
curl -O ${INSTALL_SCRIPT_URL} && \
bash falcon-linux-install.sh
"
aws ssm send-command --instance-ids "$INSTANCE_ID" \
--document-name AWS-RunShellScript \
--parameters "commands='$INSTALL_FALCON_COMMAND'"
else
echo "CrowdStrike CID missing, skipping falcon sensor setup"
fi
env:
REGION: ${{ secrets.AWS_REGION }}
report-if-failed:
name: Report if workflow failed
runs-on: ubuntu-20.04
needs: [check-secrets, lint-templates, deploy-templates, setup-rds-roles]
if: failure() && github.ref == 'refs/heads/master'
steps:
- id: send-to-slack
name: Send failure notification to Slack on failure
env:
SLACK_BOT_TOKEN: ${{ secrets.WORKFLOW_STATUS_BOT_TOKEN }}
uses: voxmedia/github-action-slack-notify-build@v1
with:
channel: workflow-failures
status: FAILED
color: danger