Deploy changed CloudFormation templates #1571
Workflow file for this run
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |