diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..093fd9e --- /dev/null +++ b/.env.template @@ -0,0 +1,21 @@ +AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY +AWS_SESSION_TOKEN + +CLUSTER_NAME +APP_NAME +AWS_DEFAULT_REGION +SERVICE_TYPE +IMAGE_NAME +CPU +MEMORY +CONTAINER_PORT +DEFAULT_COMMAND +AWS_ACCOUNT_ID +SUBNETS +DEPLOYMENT_TIMEOUT +SECURITY_GROUPS +TPL_FILE_NAME +ECR_ACCOUNT +BUILD_VERSION +SEVERITY diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 498902e..64862d0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,16 +3,11 @@ name: Lint on: [push] jobs: - lint: - name: hadolint - runs-on: ubuntu-latest - steps: - uses: actions/checkout@master - - name: hadolint uses: hadolint/hadolint-action@v1.5.0 env: diff --git a/.github/workflows/registry.yml b/.github/workflows/registry.yml index f241944..656b6ea 100644 --- a/.github/workflows/registry.yml +++ b/.github/workflows/registry.yml @@ -11,8 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 - + uses: actions/checkout@v3 - name: Docker meta id: meta uses: docker/metadata-action@v3 @@ -27,23 +26,19 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DNX_DOCKERHUB_USERNAME }} password: ${{ secrets.DNX_DOCKERHUB_TOKEN }} - - name: Login to GitHub Container Registry uses: docker/login-action@v1 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to Public ECR uses: docker/login-action@v1 with: @@ -52,7 +47,6 @@ jobs: password: ${{ secrets.AWS_ECR_SECRET_ACCESS_KEY }} env: AWS_REGION: us-east-1 - - name: Build and Push uses: docker/build-push-action@v2 with: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 1975a3d..bc84eb9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,31 +3,23 @@ name: Security on: [push] jobs: - build: - runs-on: ubuntu-latest - steps: - name: Checkout the code - uses: actions/checkout@v2 - - - name: Build the Docker image + uses: actions/checkout@v3 + - name: Build the container image run: docker build . --file Dockerfile --tag dnxsolutions/ecs-deploy:latest - - name: Scan image uses: anchore/scan-action@v3 id: scan with: - image: dnxsolutions/ecs-deploy:latest - fail-build: true + image: "dnxsolutions/ecs-deploy:latest" + fail-build: false severity-cutoff: critical - acs-report-enable: true - - name: Inspect action SARIF report run: cat ${{ steps.scan.outputs.sarif }} - - name: Upload Anchore Scan Report - uses: github/codeql-action/upload-sarif@v1 + uses: github/codeql-action/upload-sarif@v2 with: sarif_file: ${{ steps.scan.outputs.sarif }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d927ce1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +*/__pycache__ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 42e3459..30d3ec7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,12 @@ WORKDIR /work COPY src . -ENTRYPOINT [ "/bin/bash", "-c" ] +# RUN apk add libcurl=7.79.1-r5 \ +# && apk add curl=7.79.1-r5 \ +# && apk add git=2.32.6-r0 \ +# && apk add python3=3.9.16-r0 \ +# && apk add python3-dev=3.9.16-r0 -CMD [ "/work/deploy.sh" ] \ No newline at end of file +ENTRYPOINT [ "python3", "-u" ] + +CMD [ "/work/deploy.py" ] diff --git a/Makefile b/Makefile index e54a7fb..b5a8b53 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,45 @@ IMAGE_NAME ?= dnxsolutions/ecs-deploy:latest +.env: + cp .env.template .env + echo >> .env + build: docker build -t $(IMAGE_NAME) . -shell: - docker run --rm -it --entrypoint=/bin/bash -v ~/.aws:/root/.aws -v $(PWD):/opt/app $(IMAGE_NAME) +shell: .env + docker run --rm -it --env-file=.env \ + --entrypoint=/bin/bash -v ~/.aws:/root/.aws \ + -v $(PWD)/src:/work $(IMAGE_NAME) + +scan: build + docker run --rm \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --name Grype anchore/grype:v0.59.1 \ + $(IMAGE_NAME) lint: - docker run --rm -i -v $(PWD)/hadolint.yaml:/.config/hadolint.yaml hadolint/hadolint < Dockerfile \ No newline at end of file + docker run --rm -i \ + -v $(PWD)/hadolint.yaml:/.config/hadolint.yaml \ + hadolint/hadolint < Dockerfile + +deploy: .env + @echo "make deploy" + docker-compose -f docker-compose.yml run --rm deploy + +cutover: .env + @echo "make cutover" + docker-compose -f docker-compose.yml run --rm cutover + +run-task: .env + @echo "make run-task" + docker-compose -f docker-compose.yml run --rm run-task + +worker-deploy: + @echo "make worker-deploy" + docker-compose -f docker-compose.yml run --rm worker-deploy + +ecr-scan: + @echo "make ecr-scan" + docker-compose -f docker-compose.yml run --rm ecr-scan + \ No newline at end of file diff --git a/README.md b/README.md index 035e762..1485928 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ Variables must be set in the environment system level. |Variable|Type|Description|Default| |---|---|---|---| |DEPLOY_TIMEOUT|Integer|Timeout in seconds for deployment|900| -|AWS_CODE_DEPLOY_OUTPUT_STATUS_LIVE|Boolean|If the environment supports live reloading use carriage returns for a single line|True| +|TPL_FILE_NAME|Sring|Task definitions template json file name|task-definition.tpl.json| +|APPSPEC_FILE_NAME|String|CodeDeploy App Spec|app-spec.tpl.json| +|SEVERITY|List(space separated)|List of container vulnerability severity|CRITICAL HIGH| ## Usage Inside your application repository, create the following files: @@ -34,55 +36,63 @@ Inside your application repository, create the following files: # Required variables APP_NAME= CLUSTER_NAME= -IMAGE_NAME= -CONTAINER_PORT=80 -AWS_DEFAULT_REGION= +AWS_DEFAULT_REGION= + +#ECR Scanning +BUILD_VERSION= +APP_NAME= +AWS_DEFAULT_REGION= +ECR_ACCOUNT= # App-specific variables (as used on task-definition below) -DB_HOST= -DB_USER= -DB_PASSWORD= -DB_NAME= +IMAGE_NAME= +CPU= +MEMORY= +CONTAINER_PORT= +DEFAULT_COMMAND= +AWS_ACCOUNT_ID= ``` -If the service type is **Fargate**, and you're using the `run-task.sh` script, please include: +If the service type is **Fargate** please include: ```bash SERVICE_TYPE=FARGATE -SUBNETS=subnet1231231,subnet123123123,subnter123123123123 +SUBNETS=subnet-12345abcd,subnet-a1b2c3d4,subnet-abcd12345 +SECURITY_GROUPS=sg-a1b2c3d4e5,sg-12345abcd ``` -Default values are: null -`task-definition.tpl.json` (example) -```json +`task-definition.tpl.json` (see [templates](./templates/)) +```yaml { "containerDefinitions": [ { "essential": true, "image": "${IMAGE_NAME}", - "memoryReservation": 512, + "command": ${DEFAULT_COMMAND}, + "cpu": ${CPU}, + "memory": ${MEMORY}, + "memoryReservation": ${MEMORY}, "name": "${APP_NAME}", "portMappings": [ { "containerPort": ${CONTAINER_PORT} } ], + "environment": [], + "mountPoints": [], + "volumesFrom": [], "logConfiguration": { "logDriver": "awslogs", "options": { - "awslogs-group": "ecs-${CLUSTER_NAME}-${APP_NAME}", - "awslogs-region": "ap-southeast-2", - "awslogs-stream-prefix": "web" + "awslogs-group": "/ecs/${CLUSTER_NAME}/${APP_NAME}", + "awslogs-region": "${AWS_DEFAULT_REGION}", + "awslogs-stream-prefix": "${APP_NAME}" } - }, - "environment" : [ - { "name" : "DB_HOST", "value" : "${WODB_HOST}" }, - { "name" : "DB_USER", "value" : "${DB_USER}" }, - { "name" : "DB_PASSWORD", "value" : "${DB_PASSWORD}" }, - { "name" : "DB_NAME", "value" : "${DB_NAME}" } - ] + } } ], - "family": "${APP_NAME}" + "family": "${CLUSTER_NAME}-${APP_NAME}", + "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecs-task-${CLUSTER_NAME}-${AWS_DEFAULT_REGION}", + "taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecs-task-${CLUSTER_NAME}-${AWS_DEFAULT_REGION}" } ``` @@ -92,20 +102,35 @@ The Capacity Provider Strategy property specifies the details of the default cap sample: ``` -CAPACITY_PROVIDER_STRATEGY?={'Base':0,'CapacityProvider':'FARGATE_SPOT','Weight':1} +CAPACITY_PROVIDER_STRATEGY={'Base':0,'CapacityProvider':'FARGATE_SPOT','Weight':1} ``` ## Run -Run the service to deploy: +[docker-compose.yml](./docker-compose.yml) examples + +Deploy a service: ``` docker-compose run --rm deploy +docker-compose run --rm cutover +``` +Run one time task such as db migration: +``` +docker-compose run --rm run-task +``` +Run a worker service (ECS deployment): +``` +docker-compose run --rm worker-deploy +``` +Get ECR Enhanced Scan report: +``` +docker-compose run --rm ecr-scan ``` ## Caveats - Make sure the log group specified in the task definition exists in Cloudwatch Logs -- CodeDeploy Application and Deployment Group should exist and be called `$CLUSTER_NAME-$APP_NAME` +- CodeDeploy Application name and Deployment Group should exist and be called `$CLUSTER_NAME-$APP_NAME` This container is made to be used with our terraform modules: - diff --git a/docker-compose.yml b/docker-compose.yml index de932f1..4301631 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,43 @@ version: '3.4' - services: - app: + deploy: + build: . + image: public.ecr.aws/dnxsolutions/ecs-deploy:latest + env_file: + - .env + volumes: + - ./templates/task-definition.tpl-default.json:/work/task-definition.tpl.json + + cutover: + build: . + image: public.ecr.aws/dnxsolutions/ecs-deploy:latest + env_file: + - .env + command: /work/deploy-cutover.py + + run-task: + build: . + image: public.ecr.aws/dnxsolutions/ecs-deploy:latest + env_file: + - .env + command: /work/run-task.py + volumes: + - ./templates/task-definition.tpl-default.json:/work/task-definition.tpl.json + + worker-deploy: + build: . + image: public.ecr.aws/dnxsolutions/ecs-deploy:latest + env_file: + - .env + command: /work/worker-deploy.py + volumes: + - ./templates/task-definition.tpl-default.json:/work/task-definition.tpl.json + + ecr-scan: build: . + image: public.ecr.aws/dnxsolutions/ecs-deploy:latest + env_file: + - .env + command: /work/ecr-enhanced-scanning.py volumes: - - .:/work - environment: - - AWS_ACCESS_KEY_ID - - AWS_ACCOUNT_ID - - AWS_DEFAULT_REGION - - AWS_ROLE - - AWS_SECRET_ACCESS_KEY - - AWS_SECURITY_TOKEN - - AWS_SESSION_EXPIRATION - - AWS_SESSION_TOKEN - entrypoint: "" - command: /bin/bash + - ./templates/task-definition.tpl-default.json:/work/task-definition.tpl.json diff --git a/src/codedeploy.py b/src/codedeploy.py new file mode 100755 index 0000000..60cd875 --- /dev/null +++ b/src/codedeploy.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import boto3 + +class DeployClient(object): + def __init__(self): + self.boto = boto3.client(u'codedeploy') + + def list_deployments(self, application_name, deployment_group, statuses=['InProgress', 'Ready']): + result = self.boto.list_deployments( + applicationName=application_name, + deploymentGroupName=deployment_group, + includeOnlyStatuses=statuses + ) + self.deployments = result['deployments'] + return result + + def create_deployment(self, application_name, deployment_config_name, deployment_group, revision): + result = self.boto.create_deployment( + applicationName=application_name, + deploymentGroupName=deployment_group, + deploymentConfigName=deployment_config_name, + description='Deployment', + revision=revision + ) + self.deploymentId = result['deploymentId'] + return result + + def continue_deployment(self, deployment_id): + return self.boto.continue_deployment( + deploymentId=deployment_id, + deploymentWaitType='READY_WAIT' + ) + + def get_deployment(self, deployment_id): + result = self.boto.get_deployment(deploymentId=deployment_id) + self.status = result['deploymentInfo']['status'] + return result + + def stop_deployment(self, deployment_id, auto_rollback=True): + return self.boto.stop_deployment( + deploymentId=deployment_id, + autoRollbackEnabled=auto_rollback + ) diff --git a/src/deploy-cutover.py b/src/deploy-cutover.py new file mode 100755 index 0000000..3d32ab2 --- /dev/null +++ b/src/deploy-cutover.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +import os +from codedeploy import DeployClient +from utils import validate_envs + +# ----- Check variables ----- +req_vars = [ + 'CLUSTER_NAME', + 'APP_NAME', + 'AWS_DEFAULT_REGION' +] + +try: + validate_envs(req_vars) +except: + exit(1) + +cluster_name = os.getenv('CLUSTER_NAME') +app_name = os.getenv('APP_NAME') +aws_default_region = os.getenv('AWS_DEFAULT_REGION') + +# Fetch deployment 'Ready' status and cutover +deploy = DeployClient() + +application_name = '-'.join([cluster_name, app_name]) +deployment_group = application_name + +try: + deploy.list_deployments(application_name, deployment_group, ['Ready']) + deploy.continue_deployment(deploy.deployments[0]) + print('---> Cutover engaged!') +except: + print('---> ERROR: Cutover FAILED!') + exit(1) + \ No newline at end of file diff --git a/src/deploy-cutover.sh b/src/deploy-cutover.sh deleted file mode 100755 index f907eac..0000000 --- a/src/deploy-cutover.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -e - -ERROR=0 -if [[ -z "$AWS_DEFAULT_REGION" ]]; then echo "---> ERROR: Missing variable AWS_DEFAULT_REGION"; ERROR=1; fi -if [[ -z "$APP_NAME" ]]; then echo "---> ERROR: Missing variable APP_NAME"; ERROR=1; fi -if [[ -z "$CLUSTER_NAME" ]]; then echo "---> ERROR: Missing variable CLUSTER_NAME"; ERROR=1; fi -if [[ "$ERROR" == "1" ]]; then exit 1; fi - -# Fetch deployment ID pending cutover to the green(new) enviroment -DEPLOYMENT_ID=$(aws deploy list-deployments --application-name=$CLUSTER_NAME-$APP_NAME --deployment-group=$CLUSTER_NAME-$APP_NAME --max-items=1 --query="deployments[0]" --output=text | head -n 1) - -aws deploy continue-deployment --deployment-id $DEPLOYMENT_ID --deployment-wait-type "READY_WAIT" - -RET=$? - -if [ $RET -eq 0 ]; then - echo "---> Cutover engaged!" -else - echo "---> ERROR: Cutover FAILED!" -fi - -exit $RET diff --git a/src/deploy-old.sh b/src/deploy-old.sh deleted file mode 100755 index 8e0b96f..0000000 --- a/src/deploy-old.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/bin/bash -e - -# if [[ ! -f "task-definition.tpl.json" ]]; then -# echo "---> ERROR: task-definition.tpl.json not found" -# exit 0 -# fi - -# ERROR=0 -# if [[ -z "$AWS_DEFAULT_REGION" ]]; then echo "---> ERROR: Missing variable AWS_DEFAULT_REGION"; ERROR=1; fi -# if [[ -z "$APP_NAME" ]]; then echo "---> ERROR: Missing variable APP_NAME"; ERROR=1; fi -# if [[ -z "$CLUSTER_NAME" ]]; then echo "---> ERROR: Missing variable CLUSTER_NAME"; ERROR=1; fi -# if [[ -z "$CONTAINER_PORT" ]]; then echo "---> ERROR: Missing variable CONTAINER_PORT"; ERROR=1; fi -# if [[ -z "$IMAGE_NAME" ]]; then echo "---> ERROR: Missing variable IMAGE_NAME"; ERROR=1; fi -# if [[ "$ERROR" == "1" ]]; then exit 1; fi - -if [[ -z "$DEPLOY_TIMEOUT" ]]; then - echo "---> INFO: Deploy timeout set to default of 900 seconds" -else - echo "---> INFO: Deploy timeout set to ${DEPLOY_TIMEOUT} seconds"; -fi - -DEPLOY_CONCURRENCY_MODE=${DEPLOY_CONCURRENCY_MODE:-fail} -if [[ "$DEPLOY_CONCURRENCY_MODE" == "wait" ]] -then - echo "---> INFO: Deploy concurrency mode set to 'wait' a previous deployment to finish before continuing" -else - echo "---> INFO: Deploy concurrency mode set to 'fail'" -fi - -envsubst < task-definition.tpl.json > task-definition.json -echo "---> Task Definition" -cat task-definition.json - -export TASK_ARN=$(aws ecs register-task-definition --cli-input-json file://./task-definition.json | jq --raw-output '.taskDefinition.taskDefinitionArn') - -envsubst < app-spec.tpl.json > app-spec.json -echo "---> App-spec for CodeDeploy" -cat app-spec.json -echo -echo "---> Creating deployment with CodeDeploy" - -set +e # disable bash exit on error - -DEPLOY_TIMEOUT_PERIOD=0 - -while [ "${DEPLOYMENT_ID}" == "" ] -do - if [ "$DEPLOY_TIMEOUT_PERIOD" -ge "${DEPLOY_TIMEOUT:-900}" ]; then - echo "===> Timeout reached trying to create deployment. Exiting" - exit 1 - fi - - DEPLOYMENT_ID=$(aws deploy create-deployment \ - --application-name $CLUSTER_NAME-$APP_NAME \ - --deployment-config-name CodeDeployDefault.ECSAllAtOnce \ - --deployment-group-name $CLUSTER_NAME-$APP_NAME \ - --description Deployment \ - --revision file://app-spec.json \ - --query="deploymentId" --output text) - - if [ $? -eq 255 ] && [ "${DEPLOY_CONCURRENCY_MODE}" == "fail" ] - then - # In case there is already a deployment in progress, script will fail - echo - echo - echo "===> Deployment already in progress for this application environment. Please approve or rollback current deployment before performing a new deployment" - echo - echo - exit 1 - fi - - sleep 10 # Wait until deployment is created - DEPLOY_TIMEOUT_PERIOD=$((DEPLOY_TIMEOUT_PERIOD + 10)) -done - -echo "---> For more info: https://$AWS_DEFAULT_REGION.console.aws.amazon.com/codesuite/codedeploy/deployments/$DEPLOYMENT_ID" - -/work/tail-ecs-events.py & -TAIL_ECS_EVENTS_PID=$! - -RET=0 - -while [ "$(aws deploy get-deployment --deployment-id $DEPLOYMENT_ID --query deploymentInfo.status --output text)" == "Created" ] -do - sleep 1 -done - -echo "---> Deployment created!" - -DEPLOY_TIMEOUT_PERIOD=0 -DEPLOY_TIMEOUT_REACHED=0 - -while [ "$(aws deploy get-deployment --deployment-id $DEPLOYMENT_ID --query deploymentInfo.status --output text)" == "InProgress" ] -do - if [ "$DEPLOY_TIMEOUT_PERIOD" -ge "${DEPLOY_TIMEOUT:-900}" ]; then - echo "---> WARNING: Timeout reached. Rolling back deployment..." - aws deploy stop-deployment --deployment-id $DEPLOYMENT_ID --auto-rollback-enabled - DEPLOY_TIMEOUT_REACHED=1 - fi - sleep 1 - DEPLOY_TIMEOUT_PERIOD=$((DEPLOY_TIMEOUT_PERIOD + 1)) -done - -TASK_SET_ID=$(aws ecs describe-services --cluster $CLUSTER_NAME --service $APP_NAME --query "services[0].taskSets[?status == 'ACTIVE'].id" --output text) -if [ "${TASK_SET_ID}" != "" ]; then - echo "---> Task Set ID: $TASK_SET_ID" -fi - -# Due the known issue on Codedeploy, CodeDeploy will fail the deployment if the ECS service is unhealthy/unstable for 5mins for replacement -# taskset during the wait status, this 5mins is a non-configurable value as today. -# For the reason above we wait for 10 minutes before consider the deployment in ready status as successful - -WAIT_PERIOD=0 -MAX_WAIT=300 #$(aws ecs describe-services --cluster $CLUSTER_NAME --service $APP_NAME --query services[0].healthCheckGracePeriodSeconds --output text) -MAX_WAIT_BUFFER=60 - -echo "---> Waiting $((MAX_WAIT + MAX_WAIT_BUFFER)) seconds for tasks to stabilise" - -while [ "$(aws deploy get-deployment --deployment-id $DEPLOYMENT_ID --query deploymentInfo.status --output text)" == "Ready" ] -do - if [ "$WAIT_PERIOD" -ge "$((MAX_WAIT + MAX_WAIT_BUFFER))" ]; then - break - fi - sleep 10 - WAIT_PERIOD=$((WAIT_PERIOD + 10)) -done - -DEPLOYMENT_STATUS=$(aws deploy get-deployment --deployment-id $DEPLOYMENT_ID --query deploymentInfo.status --output text) -echo "---> Deployment status: $DEPLOYMENT_STATUS" - -if [ "$DEPLOYMENT_STATUS" == "Failed" ] || [ "$DEPLOYMENT_STATUS" == "Stopped" ] -then - if [ "$TASK_SET_ID" != "" ] - then - TASK_ARN=$(aws ecs list-tasks --cluster $CLUSTER_NAME --desired-status STOPPED --started-by $TASK_SET_ID --query taskArns[0] --output text) - if [ "${TASK_ARN}" != "None" ]; then - echo "---> Displaying logs of STOPPED task: $TASK_ARN" - /work/tail-task-logs.py $TASK_ARN - fi - fi - RET=1 -elif [ "$DEPLOYMENT_STATUS" == "Succeeded" ] -then - RET=0 -fi - -if [ $RET -eq 0 ]; then - echo "---> Completed!" -else - if [ $DEPLOY_TIMEOUT_REACHED -eq 1 ]; then - echo "---> Deploy timeout reached and rollback triggered." - fi - echo "---> ERROR: Deployment FAILED!" -fi - -kill $TAIL_ECS_EVENTS_PID - -exit $RET \ No newline at end of file diff --git a/src/deploy-stop.sh b/src/deploy-stop.sh deleted file mode 100755 index b32df84..0000000 --- a/src/deploy-stop.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -e - -ERROR=0 -if [[ -z "$AWS_DEFAULT_REGION" ]]; then echo "---> ERROR: Missing variable AWS_DEFAULT_REGION"; ERROR=1; fi -if [[ -z "$APP_NAME" ]]; then echo "---> ERROR: Missing variable APP_NAME"; ERROR=1; fi -if [[ -z "$CLUSTER_NAME" ]]; then echo "---> ERROR: Missing variable CLUSTER_NAME"; ERROR=1; fi -if [[ "$ERROR" == "1" ]]; then exit 1; fi - -# Fetch deployment ID pending cutover to the green(new) enviroment -DEPLOYMENT_ID=$(aws deploy list-deployments --application-name=$CLUSTER_NAME-$APP_NAME --deployment-group=$CLUSTER_NAME-$APP_NAME --max-items=1 --query="deployments[0]" --output=text | head -n 1) - -aws deploy stop-deployment --deployment-id $DEPLOYMENT_ID - -RET=$? - -if [ $RET -eq 0 ]; then - echo "---> Deployment stopped!" -else - echo "---> ERROR: Deployment stopped FAILED!" -fi - -exit $RET diff --git a/src/deploy.py b/src/deploy.py new file mode 100755 index 0000000..c1a5c6a --- /dev/null +++ b/src/deploy.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 + +import os +import json +import time +from ecs import EcsClient +from codedeploy import DeployClient +from utils import validate_envs, json_template + +# ----- Check variables ----- +print('Step 1: Checking environment variables \n') + +req_vars = [ + 'CLUSTER_NAME', + 'APP_NAME', + 'AWS_DEFAULT_REGION' +] + +try: + validate_envs(req_vars) +except: + exit(1) + +cluster_name = os.getenv('CLUSTER_NAME') +app_name = os.getenv('APP_NAME') +aws_default_region = os.getenv('AWS_DEFAULT_REGION') +launchtype = os.getenv('SERVICE_TYPE') +subnets = os.getenv('SUBNETS') +security_groups = os.getenv('SECURITY_GROUPS') +task_def_file_name = os.getenv('TPL_FILE_NAME', 'task-definition.tpl.json') +app_spec_file_name = os.getenv('APPSPEC_FILE_NAME', 'app-spec.tpl.json') +capacity_provider_strategy = os.getenv('CAPACITY_PROVIDER_STRATEGY') + +# ----- Create task definition file ----- +print('Step 2: Replace variables inside of %s \n' % task_def_file_name) + +try: + task_definition = json_template(task_def_file_name) +except: + exit(1) + +print('Task definition file: \n%s' % task_definition) +task_def = json.loads(task_definition) + +# ----- Register task definition file ----- +print('Step 3: Registering task definition \n') +task = EcsClient() + +try: + task.register_task_definition(task_def) + print('Task definition arn: %s \n' % task.taskDefArn) +except Exception as err: + print('Register task definition issue: %s' % err) + exit(1) + +# ----- Code Deploy ----- +print('Step 4: Creating App Spec for CodeDeploy \n') + +env_vars = dict(os.environ) +env_vars['TASK_ARN'] = task.taskDefArn +env_vars['CAPACITY_PROVIDER_STRATEGY'] = '' +if capacity_provider_strategy: + env_vars['CAPACITY_PROVIDER_STRATEGY'] = ',\"CapacityProviderStrategy\":[\'%s\']' % capacity_provider_strategy + +try: + app_spec_tpl = json_template(app_spec_file_name, env_vars) +except: + exit(1) + +print('App spec file: \n%s' % app_spec_tpl) +app_spec = json.loads(app_spec_tpl) + +# ----- Create Deployment ----- +print('Step 5: Creating Deployment \n') +deploy = DeployClient() + +application_name = '-'.join([cluster_name, app_name]) +deployment_config_name = 'CodeDeployDefault.ECSAllAtOnce' +deployment_group = application_name + +try: + deploy.list_deployments(application_name, deployment_group) + if len(deploy.deployments) > 0: + raise Exception('Deployment in progress: https://%s.console.aws.amazon.com/codesuite/codedeploy/deployments/%s' % + (aws_default_region, deploy.deployments[0])) +except Exception as err: + print('Error: %s' % str(err)) + exit(1) + +try: + deploy.create_deployment( + application_name, deployment_config_name, deployment_group, app_spec) + print('Successfully created deployment: %s' % deploy.deploymentId) + print('For more info, you can follow your deployment at: https://%s.console.aws.amazon.com/codesuite/codedeploy/deployments/%s \n' % + (aws_default_region, deploy.deploymentId)) +except: + print('Deployment of application %s on deployment group %s failed' % + (application_name, deployment_group)) + exit(1) + +# ----- Monitor Deployment ----- +print('Step 6: Deployment Overview \n') + +print('Monitoring deployment %s for %s on deployment group %s' % (deploy.deploymentId, application_name, deployment_group)) + +while not hasattr(task, 'taskSetId'): + # set task.taskSetId + task.describe_services(cluster_name, app_name) + time.sleep(2) + +print('Task Set ID: %s \n' % task.taskSetId) + +print('Monitoring ECS service events for cluster %s on service %s:\n' % (cluster_name, app_name)) + +deploy_timeout_period = 0 +deploy_timeout = int(os.getenv('DEPLOYMENT_TIMEOUT', 900)) + +# deploy.status +deploy.get_deployment(deploy.deploymentId) + +while deploy.status in ['Created', 'InProgress']: + # Tail logs from ECS service + ecs_events = task.tail_ecs_events(cluster_name, app_name) + for event in ecs_events: + print('%s %s' % ('{0:%Y-%m-%d %H:%M:%S %z}'.format(event['createdAt']), event['message'])) + + # Check if containers are being stoped + last_task = task.list_tasks(cluster_name, task.taskSetId) + if len(last_task['taskArns']) > 2: + last_task_info = task.describe_tasks(cluster_name, last_task['taskArns']) + last_task_status = last_task_info['tasks'][0]['lastStatus'] + last_task_reason = last_task_info['tasks'][0]['stoppedReason'] + + if last_task_status == 'STOPPED': + print('Containers are being stoped: %s' % last_task_reason) + try: + deploy.stop_deployment(deploy.deploymentId) + print('Rollback deployment success') + except: + print('Rollback deployment failed') + finally: + exit(1) + + # Rechead limit + if deploy_timeout_period >= deploy_timeout: + print('Deployment timeout: %s seconds' % deploy_timeout) + try: + deploy.stop_deployment(deploy.deploymentId) + print('Rollback deployment success') + except: + print('Rollback deployment failed') + finally: + exit(1) + + # Get status, increment limit and sleep + deploy.get_deployment(deploy.deploymentId) + deploy_timeout_period += 2 + time.sleep(2) + +# Print Status +deployment_info = deploy.get_deployment(deploy.deploymentId) + +print() +if deploy.status not in ['Ready', 'Succeeded']: + print('Deployment failed: %s' % deployment_info['deploymentInfo']['errorInformation']['code']) + print('Error: %s' % deployment_info['deploymentInfo']['errorInformation']['message']) + +if deploy.status == "Ready": + print('Deployment of application %s on deployment group %s ready and waiting for cutover' % (application_name, deployment_group)) + +if deploy.status == "Succeeded": + print('Deployment of application %s on deployment group %s succeeded' % (application_name, deployment_group)) + \ No newline at end of file diff --git a/src/deploy.sh b/src/deploy.sh deleted file mode 100755 index bf0012e..0000000 --- a/src/deploy.sh +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env bash - -set +e -set -o noglob - - -# -# Set Colors -# - -bold="\e[1m" -dim="\e[2m" -underline="\e[4m" -blink="\e[5m" -reset="\e[0m" -red="\e[31m" -green="\e[32m" -blue="\e[34m" - - -# -# Common Output Styles -# - -h1() { - printf "\n${bold}${underline}%s${reset}\n" "$(echo "$@" | sed '/./,$!d')" -} -h2() { - printf "\n${bold}%s${reset}\n" "$(echo "$@" | sed '/./,$!d')" -} -info() { - printf "${dim}➜ %s${reset}\n" "$(echo "$@" | sed '/./,$!d')" -} -success() { - printf "${green}✔ %s${reset}\n" "$(echo "$@" | sed '/./,$!d')" -} -error() { - printf "${red}${bold}✖ %s${reset}\n" "$(echo "$@" | sed '/./,$!d')" -} -warnError() { - printf "${red}✖ %s${reset}\n" "$(echo "$@" | sed '/./,$!d')" -} -warnNotice() { - printf "${blue}✖ %s${reset}\n" "$(echo "$@" | sed '/./,$!d')" -} -note() { - printf "\n${bold}${blue}Note:${reset} ${blue}%s${reset}\n" "$(echo "$@" | sed '/./,$!d')" -} - -# Runs the specified command and logs it appropriately. -# $1 = command -# $2 = (optional) error message -# $3 = (optional) success message -# $4 = (optional) global variable to assign the output to -runCommand() { - command="$1" - info "$1" - output="$(eval $command 2>&1)" - ret_code=$? - - if [ $ret_code != 0 ]; then - warnError "$output" - if [ ! -z "$2" ]; then - error "$2" - fi - exit $ret_code - fi - - if [ ! -z "$3" ]; then - success "$3" - fi - - if [ ! -z "$4" ]; then - eval "$4='$output'" - fi -} - -typeExists() { - if [ $(type -P $1) ]; then - return 0 - fi - return 1 -} - -jsonValue() { - key=$1 - num=$2 - awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'$key'\042/){print $(i+1)}}}' | tr -d '"' | sed -n ${num}p -} - -vercomp() { - if [[ $1 == $2 ]] - then - return 0 - fi - local IFS=. - local i ver1=($1) ver2=($2) - - # fill empty fields in ver1 with zeros - for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)) - do - ver1[i]=0 - done - - for ((i=0; i<${#ver1[@]}; i++)) - do - if [[ -z ${ver2[i]} ]] - then - # fill empty fields in ver2 with zeros - ver2[i]=0 - fi - if ((10#${ver1[i]} > 10#${ver2[i]})) - then - return 1 - fi - if ((10#${ver1[i]} < 10#${ver2[i]})) - then - return 2 - fi - done - return 0 -} - -isjsonValid() { - file=$1 - info "Verifying if file $file is a valid JSON" - if [ $(cat $file | jq empty > /dev/null 2>&1; echo $?) -eq 0 ]; then - success "File $file is a valid JSON file" - else - error "File $file is not a valid JSON file" - fi - return $? -} - -# ----- Check variables ----- -h1 "Step 1: Checking environment variables" - -if [[ ! -f "task-definition.tpl.json" ]]; then - error "File: task-definition.tpl.json not found" - exit 1 -fi - -if [ -z "$AWS_DEFAULT_REGION" ]; then - error "Please set the \"\$AWS_DEFAULT_REGION\" variable" - exit 1 -fi - -if [ -z "$APP_NAME" ]; then - error "Please set the \"\$APP_NAME\" variable" - exit 1 -fi - -if [ -z "$CLUSTER_NAME" ]; then - error "Please set the \"\$CLUSTER_NAME\" variable" - exit 1 -fi - -if [ -z "$CONTAINER_PORT" ]; then - error "Please set the \"\$CONTAINER_PORT\" variable" - exit 1 -fi - -if [ -z "$IMAGE_NAME" ]; then - error "Please set the \"\$IMAGE_NAME\" variable" - exit 1 -fi - -success "Variables ok" - -# ----- Create task definition file ----- -h1 "Step 2: Replace variables inside of task-definition.tpl.json" -runCommand "envsubst < task-definition.tpl.json > task-definition.json" \ - "Create task definition file failed" \ - "Create task definition file" - -isjsonValid "task-definition.json" -info "Task definition file:" -cat task-definition.json | jq - -# ----- Register task definition file ----- -h1 "Step 3: Registering task definition" -runCommand "aws ecs register-task-definition --cli-input-json file://./task-definition.json" \ - "Register task definition failed" \ - "Register task definition" \ - OUTPUT_TASK_ARN - -OUTPUT_TASK_ARN=$(echo $OUTPUT_TASK_ARN | jq --raw-output '.taskDefinition.taskDefinitionArn') -export TASK_ARN=$OUTPUT_TASK_ARN - -# ----- Create app spec file ----- -if [ ! -z "$CAPACITY_PROVIDER_STRATEGY" ]; then - CAPACITY_PROVIDER_STRATEGY=',\"CapacityProviderStrategy\":['${CAPACITY_PROVIDER_STRATEGY}']' -fi - -h1 "Step 4: Creating App Spec for CodeDeploy" -runCommand "envsubst < app-spec.tpl.json > app-spec.json" \ - "Create app-spec file failed" \ - "Create app-spec file" - -isjsonValid "app-spec.json" -info "App spec file:" -cat app-spec.json | jq - -# ----- Create Deployment ----- -h1 "Step 5: Creating Deployment" -APPLICATION_NAME=$CLUSTER_NAME-$APP_NAME -DEPLOYMENT_CONFIG_NAME=CodeDeployDefault.ECSAllAtOnce -DEPLOYMENT_GROUP=$CLUSTER_NAME-$APP_NAME - -# TODO: Check if is there any deployment in progress - -DEPLOYMENT_CMD="aws deploy create-deployment \ - --output json \ - --application-name $APPLICATION_NAME \ - --deployment-config-name $DEPLOYMENT_CONFIG_NAME \ - --deployment-group-name $DEPLOYMENT_GROUP \ - --description Deployment \ - --revision file://app-spec.json" - -DEPLOYMENT_OUTPUT="" -runCommand "$DEPLOYMENT_CMD" \ - "Deployment of application \"$APPLICATION_NAME\" on deployment group \"$DEPLOYMENT_GROUP\" failed" \ - "" \ - DEPLOYMENT_OUTPUT - -DEPLOYMENT_ID=$(echo $DEPLOYMENT_OUTPUT | jsonValue 'deploymentId' | tr -d ' ') -success "Successfully created deployment: \"$DEPLOYMENT_ID\"" -note "For more info, you can follow your deployment at: https://$AWS_DEFAULT_REGION.console.aws.amazon.com/codesuite/codedeploy/deployments/$DEPLOYMENT_ID" - - -# ----- Monitor Deployment ----- -h1 "Step 6: Deployment Overview" - -DEPLOY_TIMEOUT_PERIOD=0 - -DEPLOYMENT_GET="aws deploy get-deployment --output json --deployment-id \"$DEPLOYMENT_ID\"" -h2 "Monitoring deployment \"$DEPLOYMENT_ID\" for \"$APPLICATION_NAME\" on deployment group $DEPLOYMENT_GROUP ..." -info "$DEPLOYMENT_GET" - -TASK_SET_ID="" - -while [ "${TASK_SET_ID}" == "" ]; do - TASK_SET_ID=$(aws ecs describe-services --cluster $CLUSTER_NAME --service $APP_NAME --query "services[0].taskSets[?status == 'ACTIVE'].id" --output text) - sleep 1 -done - -info "Task Set ID: $TASK_SET_ID" - -# If the environment supports live reloading use carriage returns for a single line. -if [ "true" == "${AWS_CODE_DEPLOY_OUTPUT_STATUS_LIVE:-true}" ]; then - status_opts="\r${bold}" - status_opts_live="\r${bold}${blink}" -else - status_opts="\n${bold}" - status_opts_live="\n${bold}" -fi - -h2 "Monitoring ECS service events for cluster ($CLUSTER_NAME) on service ($APP_NAME):" -/work/tail-ecs-events.py & TAIL_ECS_EVENTS_PID=$! -printf "\n" - -while : - do - DEPLOYMENT_GET_OUTPUT="$(eval $DEPLOYMENT_GET 2>&1)" - if [ $? != 0 ]; then - warnError "$DEPLOYMENT_GET_OUTPUT" - error "Deployment of application \"$APPLICATION_NAME\" on deployment group \"$DEPLOYMENT_GROUP\" failed" - exit 1 - fi - - # Deployment Overview - IN_PROGRESS=$(echo "$DEPLOYMENT_GET_OUTPUT" | jsonValue "InProgress" | tr -d "\r\n ") - PENDING=$(echo "$DEPLOYMENT_GET_OUTPUT" | jsonValue "Pending" | tr -d "\r\n ") - SKIPPED=$(echo "$DEPLOYMENT_GET_OUTPUT" | jsonValue "Skipped" | tr -d "\r\n ") - SUCCEEDED=$(echo "$DEPLOYMENT_GET_OUTPUT" | jsonValue "Succeeded" | tr -d "\r\n ") - FAILED=$(echo "$DEPLOYMENT_GET_OUTPUT" | jsonValue "Failed" | tr -d "\r\n ") - - # Deployment Status - STATUS=$(echo "$DEPLOYMENT_GET_OUTPUT" | jsonValue "status" | tr -d "\r\n" | tr -d " ") - ERROR_MESSAGE=$(echo "$DEPLOYMENT_GET_OUTPUT" | jsonValue "message") - - # Check if containers are being stoped - LAST_TASK_ARN=$(aws ecs list-tasks --cluster $CLUSTER_NAME --desired-status STOPPED --started-by $TASK_SET_ID --query taskArns[0] --output text) - if [ "${LAST_TASK_ARN}" != "None" ]; then - LAST_TASK_INFO=$(aws ecs describe-tasks --cluster $CLUSTER_NAME --tasks $LAST_TASK_ARN --query tasks[0]) - LAST_TASK_STATUS=$(echo $LAST_TASK_INFO | jq -r .lastStatus) - LAST_TASK_REASON=$(echo $LAST_TASK_INFO | jq -r .stoppedReason) - - if [ "${LAST_TASK_STATUS}" == "STOPPED" ]; then - runCommand "aws deploy stop-deployment --deployment-id $DEPLOYMENT_ID --auto-rollback-enabled" \ - "Rollback deployment failed" \ - "Rollback deployment success" - STATUS=Failed - ERROR_MESSAGE=$LAST_TASK_REASON - fi - fi - - - # Rechead limit - if [ "$DEPLOY_TIMEOUT_PERIOD" -ge "${DEPLOY_TIMEOUT:-900}" ]; then - warnNotice "Timeout reached. Rolling back deployment..." - runCommand "aws deploy stop-deployment --deployment-id $DEPLOYMENT_ID --auto-rollback-enabled" \ - "Rollback deployment failed" \ - "Rollback deployment success" - exit 1 - fi - - # Print Status - if [ "$STATUS" == "Failed" ]; then - error "Deployment failed: $ERROR_MESSAGE" - exit 1 - fi - - if [ "$STATUS" == "Stopped" ]; then - warnNotice "Deployment stopped by user" - info "$ERROR_MESSAGE" - exit 1 - fi - - if [ "$STATUS" == "Ready" ]; then - success "Deployment of application \"$APPLICATION_NAME\" on deployment group \"$DEPLOYMENT_GROUP\" ready and waiting for cutover" - break - fi - - if [ "$STATUS" == "Succeeded" ]; then - success "Deployment of application \"$APPLICATION_NAME\" on deployment group \"$DEPLOYMENT_GROUP\" succeeded" - break - fi - - # Increment timeout limit - ((DEPLOY_TIMEOUT_PERIOD=DEPLOY_TIMEOUT_PERIOD+1)) - sleep 1 - done - -# Kill PID from tail ecs events -kill $TAIL_ECS_EVENTS_PID \ No newline at end of file diff --git a/src/ecr-enhanced-scanning.py b/src/ecr-enhanced-scanning.py index bc46113..ef313e9 100755 --- a/src/ecr-enhanced-scanning.py +++ b/src/ecr-enhanced-scanning.py @@ -1,43 +1,57 @@ #!/usr/bin/env python3 import os -import boto3 -import json -import sys - -build_version=os.environ['BUILD_VERSION'] -severity=list(os.environ['SEVERITY'].split(' ')) -app_name=os.environ['APP_NAME'] -ecr_account=os.environ['ECR_ACCOUNT'] - -client = boto3.client('ecr') -response = client.describe_image_scan_findings( - registryId=ecr_account, - repositoryName=app_name, - imageId={ - 'imageTag': build_version - }, -) - -countResponse = len(response['imageScanFindings']['enhancedFindings']) +from ecr import EcrClient +from utils import validate_envs + +# ----- Check variables ----- +req_vars = [ + 'BUILD_VERSION', + 'APP_NAME', + 'AWS_DEFAULT_REGION', + 'ECR_ACCOUNT' +] + +try: + validate_envs(req_vars) +except: + exit(1) + +build_version = os.getenv('BUILD_VERSION') +severity = list(os.getenv('SEVERITY', 'CRITICAL HIGH').split(' ')) +app_name = os.getenv('APP_NAME') +ecr_account = os.getenv('ECR_ACCOUNT') + +try: + ecr = EcrClient() + response = ecr.describe_image_scan_findings( + ecr_account, app_name, build_version) + if 'enhancedFindings' not in response['imageScanFindings']: + raise Exception('ECR Enhanced Findings not enabled') + countResponse = len(response['imageScanFindings']['enhancedFindings']) +except Exception as err: + print('ERROR: %s' % str(err)) + exit(1) if countResponse == 0: print("---> No vulnerabilities found") -else: - print("---> Checking for %s vulnerabilities" %(severity)) +else: + print("---> Checking for %s vulnerabilities" % (severity)) for level in severity: - print ("\n" + "---> List of " + level + " packages") + print("\n" + "---> List of " + level + " packages") level_counter = 0 - for vuln_counter in range(0,countResponse): - vuln_report=response + for vuln_counter in range(0, countResponse): + vuln_report = response if vuln_report['imageScanFindings']['enhancedFindings'][vuln_counter]['severity'] == level: - print("%s: Package %s:%s" %(level,vuln_report['imageScanFindings']['enhancedFindings'][vuln_counter]['packageVulnerabilityDetails']['vulnerablePackages'][0]['name'],vuln_report['imageScanFindings']['enhancedFindings'][vuln_counter]['packageVulnerabilityDetails']['vulnerablePackages'][0]['version'])) - level_counter+=1 + print("%s: Package %s:%s" % (level, vuln_report['imageScanFindings']['enhancedFindings'][vuln_counter]['packageVulnerabilityDetails']['vulnerablePackages'] + [0]['name'], vuln_report['imageScanFindings']['enhancedFindings'][vuln_counter]['packageVulnerabilityDetails']['vulnerablePackages'][0]['version'])) + level_counter += 1 if level_counter > 0: - print("--> Total of %s vulnerabilities %s" %(level,level_counter)) + print("--> Total of %s vulnerabilities %s" % + (level, level_counter)) else: - print("--> %s vulnerabilities have not been found" %(level)) - - print("\n" + "---> WARNING: Overview of %s container image vulnerability(ies)" %(app_name)) - print(vuln_report['imageScanFindings']['findingSeverityCounts']) \ No newline at end of file + print("--> %s vulnerabilities have not been found" % (level)) + + print("\n" + "---> WARNING: Overview of %s container image vulnerability(ies)" % (app_name)) + print(vuln_report['imageScanFindings']['findingSeverityCounts']) diff --git a/src/ecr.py b/src/ecr.py new file mode 100644 index 0000000..bcd4fcb --- /dev/null +++ b/src/ecr.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import boto3 + +class EcrClient(object): + def __init__(self): + self.boto = boto3.client('ecr') + + def describe_image_scan_findings(self, ecr_account, app_name, build_version): + return self.boto.describe_image_scan_findings( + registryId=ecr_account, + repositoryName=app_name, + imageId={ + 'imageTag': build_version + } + ) + \ No newline at end of file diff --git a/src/ecs.py b/src/ecs.py new file mode 100755 index 0000000..3381ca2 --- /dev/null +++ b/src/ecs.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 + +import boto3 + +LAUNCH_TYPE_FARGATE = 'FARGATE' + +class EcsClient(object): + def __init__(self): + self.boto = boto3.client('ecs') + self.logs = boto3.client('logs') + self._last_event = None + self._log_next_token = None + + def update_service(self, cluster_name, app_name, task_definition, force_deployment=False): + return self.boto.update_service( + cluster=cluster_name, + service=app_name, + taskDefinition=task_definition, + forceNewDeployment=force_deployment + ) + + def describe_services(self, cluster_name, app_name): + result = self.boto.describe_services( + cluster=cluster_name, + services=[app_name] + ) + + if 'taskSets' in result['services'][0]: + for taskSet in result['services'][0]['taskSets']: + if taskSet['status'] == 'ACTIVE': + self.taskSetId = taskSet['id'] + + if 'deployments' in result['services'][0]: + for deployment in result['services'][0]['deployments']: + if deployment['status'] == 'PRIMARY': + self.ecsDeployId = deployment['id'] + + return result + + def register_task_definition(self, task_definition): + result = self.boto.register_task_definition( + **task_definition + ) + self.taskDefArn = result['taskDefinition']['taskDefinitionArn'] + return result + + def describe_task_definition(self, task_definition): + result = self.boto.describe_task_definition( + taskDefinition=task_definition + ) + self.taskDefArn = result['taskDefinition']['taskDefinitionArn'] + return result + + def list_tasks(self, cluster_name, started_by, desired_status='STOPPED'): + return self.boto.list_tasks( + cluster=cluster_name, + startedBy=started_by, + desiredStatus=desired_status + ) + + def describe_tasks(self, cluster_name, task_arns): + result = self.boto.describe_tasks(cluster=cluster_name, tasks=task_arns) + self.status = result['tasks'][0]['lastStatus'] + return result + + def run_task(self, cluster_name, task_definition, launchtype, subnets, security_groups): + if launchtype == LAUNCH_TYPE_FARGATE: + if not subnets or not security_groups: + msg = 'At least one subnet and one security ' \ + 'group definition are required ' \ + 'for launch type FARGATE' + raise Exception(msg) + + network_configuration = { + "awsvpcConfiguration": { + "subnets": subnets, + "securityGroups": security_groups, + "assignPublicIp": "DISABLED" + } + } + + result = self.boto.run_task( + cluster=cluster_name, + taskDefinition=task_definition, + launchType=launchtype, + networkConfiguration=network_configuration + ) + + else: + result = self.boto.run_task( + cluster=cluster_name, + taskDefinition=task_definition + ) + + self.taskArn = result['tasks'][0]['taskArn'] + self.taskId = self.taskArn.split('/')[-1] + self.status = result['tasks'][0]['lastStatus'] + return result + + def describe_log_streams(self, log_group_name): + return self.logs.describe_log_streams( + logGroupName=log_group_name, orderBy='LastEventTime', descending=True, limit=1) + + def get_log_events(self, log_args): + return self.logs.get_log_events(**log_args) + + def tail_log_events(self, log_group_name, log_stream_name): + log_args = { + 'logGroupName': log_group_name, + 'logStreamName': log_stream_name, + 'startFromHead': True + } + + if self._log_next_token: + log_args['nextToken'] = self._log_next_token + + log_stream_events = self.get_log_events(log_args) + + self._log_next_token = log_stream_events['nextForwardToken'] + return log_stream_events['events'] + + def tail_ecs_events(self, cluster_name, app_name): + get_events = self.describe_services(cluster_name, app_name) + events = get_events['services'][0]['events'] + events_collected = [] + + for event in events: + if not self._last_event or event['id'] == self._last_event: + break + events_collected.insert(0, event) + + self._last_event = events[0]['id'] + return events_collected + \ No newline at end of file diff --git a/src/register-task-definition.sh b/src/register-task-definition.sh deleted file mode 100755 index 703ba3e..0000000 --- a/src/register-task-definition.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -e - -if [[ ! -f "task-definition.tpl.json" ]]; then - echo "---> ERROR: task-definition.tpl.json not found" - exit 0 -fi - -ERROR=0 -if [[ -z "$AWS_DEFAULT_REGION" ]]; then echo "---> ERROR: Missing variable AWS_DEFAULT_REGION"; ERROR=1; fi -if [[ -z "$APP_NAME" ]]; then echo "---> ERROR: Missing variable APP_NAME"; ERROR=1; fi -if [[ -z "$CLUSTER_NAME" ]]; then echo "---> ERROR: Missing variable CLUSTER_NAME"; ERROR=1; fi -if [[ -z "$IMAGE_NAME" ]]; then echo "---> ERROR: Missing variable IMAGE_NAME"; ERROR=1; fi -if [[ "$ERROR" == "1" ]]; then exit 1; fi - -envsubst < task-definition.tpl.json > task-definition.json -echo "---> Task Definition" -cat task-definition.json - -echo "" -echo "---> Registering Task Definition" - -# Register the ecs task defination - -TASK_ARN=$(aws ecs register-task-definition \ - --cli-input-json file://task-definition.json \ - --query="taskDefinition.taskDefinitionArn" \ - --output=text) - -echo "---> Task Definition ARN: ${TASK_ARN}" - -exit diff --git a/src/run-task.py b/src/run-task.py new file mode 100755 index 0000000..f2bc80c --- /dev/null +++ b/src/run-task.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 + +import os +import json +import random +import time +from ecs import EcsClient +from utils import validate_envs, json_template + +# ----- Check variables ----- +print('Step 1: Checking environment variables \n') + +req_vars = [ + 'CLUSTER_NAME', + 'APP_NAME', + 'AWS_DEFAULT_REGION' +] + +try: + validate_envs(req_vars) +except: + exit(1) + +cluster_name = os.getenv('CLUSTER_NAME') +app_name = os.getenv('APP_NAME') +launchtype = os.getenv('SERVICE_TYPE') +subnets = os.getenv('SUBNETS') +security_groups = os.getenv('SECURITY_GROUPS') +task_def_file_name = os.getenv('TPL_FILE_NAME', 'task-definition.tpl.json') +env_vars = dict(os.environ) + +# ----- Create task definition file ----- +print('Step 2: Replace variables inside of %s \n' % task_def_file_name) + +try: + task_definition = json_template(task_def_file_name) +except: + exit(1) + +print('Task definition file: \n%s' % task_definition) +task_def = json.loads(task_definition) + +# ----- Register task definition file ----- +print('Step 3: Registering task definition \n') +task = EcsClient() + +try: + task.register_task_definition(task_def) + print('Task definition arn: %s \n' % task.taskDefArn) +except Exception as err: + print('Register task definition issue: %s' % err) + exit(1) + +# ----- Run task ----- +print('Step 4: Running task') + +if subnets: + subnets = subnets.split(",") +if security_groups: + security_groups = security_groups.split(",") + +try: + task.run_task(cluster_name, task.taskDefArn, + launchtype, subnets, security_groups) +except Exception as err: + print(err) + exit(1) + +task_time = 0 +while task.status not in ['RUNNING', 'STOPPED']: + try: + running_task = task.describe_tasks(cluster_name, [task.taskArn]) + sleep = (2 * random.uniform(0, 3)) + task_time = task_time + int(sleep) + time.sleep(sleep) + except: + print("Error get runnning task") + exit(1) +else: + print('Provisioning time: %s seconds' % task_time) + +print('\n======== RUNNING TASK ========') +print('CLUSTER_NAME: %s' % cluster_name) +print('APP_NAME: %s' % app_name) +print('TASK_DEF_ARN: %s' % task.taskDefArn) +print('TASK_ARN: %s' % task.taskArn) + +log_msg = None +last_logs = None + +print('\n======== TASK LOGS ========') +while True: + try: + log_group_name = task_def['containerDefinitions'][0]['logConfiguration']['options']['awslogs-group'] + log_prefix = task_def['containerDefinitions'][0]['logConfiguration']['options']['awslogs-stream-prefix'] + log_stream_name = '/'.join([log_prefix, app_name, task.taskId]) + + log_events = task.tail_log_events(log_group_name, log_stream_name) + for event in log_events: + print(event['message']) + except: + if not log_msg: + log_msg = 'No logs sent to CloudWatch' + print(log_msg) + finally: + running_task = task.describe_tasks(cluster_name, [task.taskArn]) + sleep = (2 * random.uniform(0, 3)) + time.sleep(sleep) + + if last_logs: + break + + if task.status == 'STOPPED': + last_logs = True + continue + +print('\n======== TASK STOPPED ========') +print('Task ID: %s' % task.taskId) +print('Task ARN: %s' % task.taskArn) +print('Service Name: %s' % app_name) +print('Cluster Name: %s' % cluster_name) +if 'startedAt' in running_task['tasks'][0]: + print('Started at: %s' % running_task['tasks'][0]['startedAt']) +print('Stopped at: %s' % running_task['tasks'][0]['stoppedAt']) +print('Stopped Reason: %s' % running_task['tasks'][0]['stoppedReason']) +if 'stopCode' in running_task['tasks'][0]: + print('Stop Code: %s' % running_task['tasks'][0]['stopCode']) +if 'exitCode' in running_task['tasks'][0]['containers'][0]: + print('Exit code: %s' %running_task['tasks'][0]['containers'][0]['exitCode']) +if 'reason' in running_task['tasks'][0]['containers'][0]: + print('Reason: %s' %running_task['tasks'][0]['containers'][0]['reason']) diff --git a/src/run-task.sh b/src/run-task.sh deleted file mode 100755 index 5d7139e..0000000 --- a/src/run-task.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash -e - -if [[ ! -f "task-definition.tpl.json" ]]; then - echo "---> ERROR: task-definition.tpl.json not found" - exit 0 -fi - -ERROR=0 -if [[ -z "$AWS_DEFAULT_REGION" ]]; then echo "---> ERROR: Missing variable AWS_DEFAULT_REGION"; ERROR=1; fi -if [[ -z "$APP_NAME" ]]; then echo "---> ERROR: Missing variable APP_NAME"; ERROR=1; fi -if [[ -z "$CLUSTER_NAME" ]]; then echo "---> ERROR: Missing variable CLUSTER_NAME"; ERROR=1; fi -if [[ -z "$IMAGE_NAME" ]]; then echo "---> ERROR: Missing variable IMAGE_NAME"; ERROR=1; fi -if [[ "$ERROR" == "1" ]]; then exit 1; fi - -envsubst < task-definition.tpl.json > task-definition.json -echo "---> Task Definition" -cat task-definition.json - -echo "" -echo "---> Registering Task Definition" - -# Update the ECS service to use the updated Task version - -TASK_ARN=$(aws ecs register-task-definition \ - --cli-input-json file://task-definition.json \ - --query="taskDefinition.taskDefinitionArn" \ - --output=text) - -echo "---> Executing ECS Task" -echo " CLUSTER_NAME: ${CLUSTER_NAME}" -echo " APP_NAME: ${APP_NAME}" -echo " TASK_DEFINITION_ARN: ${TASK_ARN}" -echo -n " STATUS: " - -# Check if cluster type is fargate -if [[ "$SERVICE_TYPE" == "FARGATE" ]]; then -TASK_ID=$(aws ecs run-task \ - --cluster $CLUSTER_NAME \ - --task-definition $TASK_ARN \ - --network-configuration "awsvpcConfiguration={subnets=[${SUBNETS}],securityGroups=[${SECURITY_GROUPS}]}" \ - --launch-type FARGATE \ - --query="tasks[0].taskArn" \ - --output=text) -else -TASK_ID=$(aws ecs run-task \ - --cluster $CLUSTER_NAME \ - --task-definition $TASK_ARN \ - --query="tasks[0].taskArn" \ - --output=text) - fi - -sleep 5 - -while [ "$(aws ecs describe-tasks --tasks $TASK_ID --cluster $CLUSTER_NAME --query="tasks[0].lastStatus" --output=text)" == "PENDING" ] -do - echo -n "." - sleep 5 -done -echo -n "RUNNING" -echo - -echo "---> Task ARN $TASK_ID" - -./tail-task-logs.py $TASK_ID - -# Discovery the Container Retunr status after the run-task -CONTAINER_EXIT_CODE=$(aws ecs describe-tasks \ - --tasks $TASK_ID \ - --cluster $CLUSTER_NAME \ - --query="tasks[0].containers[0].exitCode" \ - --output=text) -echo "---> Task Exit Code: $CONTAINER_EXIT_CODE" -RET=$CONTAINER_EXIT_CODE - - -if [ "$RET" = "0" ]; then - echo "---> TaskStatus completed!" -else - echo "---> ERROR: TaskStatus FAILED!" - RET=1 -fi - -exit $RET diff --git a/src/tail-ecs-events.py b/src/tail-ecs-events.py deleted file mode 100755 index 4b45a83..0000000 --- a/src/tail-ecs-events.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 - -import boto3, json, time, os, datetime - -aws_ecs = boto3.client('ecs') - -cluster_name=os.environ['CLUSTER_NAME'] -app_name=os.environ['APP_NAME'] - -last_event = None - -while True: - try: - response = aws_ecs.describe_services( - cluster=cluster_name, - services=[ - app_name - ] - ) - - events = response['services'][0]['events'] - events_collected = [] - - for event in events: - if not last_event or event['id'] == last_event: - break - - events_collected.insert(0, event) - - for event_collected in events_collected: - print('%s %s' % ('{0:%Y-%m-%d %H:%M:%S %z}'.format(event_collected['createdAt']), event_collected['message'])) - - last_event = events[0]['id'] - time.sleep(5) - - except Exception as e: - print("error: " + str(e)) - - diff --git a/src/tail-task-logs.py b/src/tail-task-logs.py deleted file mode 100755 index 30c0d18..0000000 --- a/src/tail-task-logs.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 - -import boto3, json, time, os, datetime, sys - -aws_ecs = boto3.client('ecs') -logs = boto3.client('logs') - -cluster_name=os.environ['CLUSTER_NAME'] -app_name=os.environ['APP_NAME'] -task_arn=sys.argv[1] - -task_id=task_arn.split("/")[-1] #get the task number id (without the cluster name) -last_event = None -log_group_name='/ecs/'+cluster_name+'/'+app_name -log_stream_prefix = None -log_stream_events = None - -print("======== TASK LOGS ========") - -while True: - try: - if log_stream_prefix is None: - try: - log_streams = logs.describe_log_streams(logGroupName=log_group_name, orderBy='LastEventTime', descending=True, limit=1) - except: - raise Exception("The specified log group does not exist") - - if len(log_streams['logStreams']) != 0: - log_stream_prefix='/'.join(log_streams['logStreams'][0]['logStreamName'].split('/')[:-1]) - extra_args = { - 'logGroupName': log_group_name, - 'logStreamName': log_stream_prefix+'/'+task_id, - 'startFromHead': True - } - else: - try: - log_stream_events = logs.get_log_events(**extra_args) - - for event in log_stream_events['events']: - print("%s" % (event['message'])) - - if 'nextToken' not in extra_args or log_stream_events['nextForwardToken'] != extra_args['nextToken']: - extra_args['nextToken'] = log_stream_events['nextForwardToken'] - - except: - print('No logs sent to CloudWatch') - - response = aws_ecs.describe_tasks( - cluster=cluster_name, - tasks=[task_arn]) - - if response['tasks'][0]['lastStatus'] == "STOPPED": - print("======== TASK STOPPED ========") - print("Task ID: %s" % task_id) - print("Task ARN: %s" % task_arn) - print("Service Name: %s" % app_name) - print("Cluster Name: %s" % cluster_name) - if 'startedAt' in response['tasks'][0]: - print("Started at: %s" % response['tasks'][0]['startedAt']) - print("Stopped at: %s" % response['tasks'][0]['stoppedAt']) - print("Stopped Reason: %s" % response['tasks'][0]['stoppedReason']) - if 'stopCode' in response['tasks'][0]: - print("Stop Code: %s" % response['tasks'][0]['stopCode']) - print("") - break - - except logs.exceptions.ResourceNotFoundException as e: - time.sleep(5) - continue - - except Exception as e: - print("Error: " + str(e)) - break - diff --git a/src/task-deploy.sh b/src/task-deploy.sh deleted file mode 100755 index 48727f1..0000000 --- a/src/task-deploy.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -e - -if [[ ! -f "task-definition.tpl.json" ]]; then - echo "---> ERROR: task-definition.tpl.json not found" - exit 0 -fi - -ERROR=0 -if [[ -z "$AWS_DEFAULT_REGION" ]]; then echo "---> ERROR: Missing variable AWS_DEFAULT_REGION"; ERROR=1; fi -if [[ -z "$APP_NAME" ]]; then echo "---> ERROR: Missing variable APP_NAME"; ERROR=1; fi -if [[ -z "$CLUSTER_NAME" ]]; then echo "---> ERROR: Missing variable CLUSTER_NAME"; ERROR=1; fi -if [[ -z "$CONTAINER_PORT" ]]; then echo "---> ERROR: Missing variable CONTAINER_PORT"; ERROR=1; fi -if [[ -z "$IMAGE_NAME" ]]; then echo "---> ERROR: Missing variable IMAGE_NAME"; ERROR=1; fi -if [[ "$ERROR" == "1" ]]; then exit 1; fi - -envsubst < task-definition.tpl.json > task-definition.json -echo "---> Task Definition" -cat task-definition.json - -export TASK_ARN=TASK_ARN_PLACEHOLDER - -echo "---> Registering Task Definition" - -# Update the ECS service to use the updated Task version - -aws ecs register-task-definition \ ---cli-input-json file://task-definition.json - -RET=$? - -if [ $RET -eq 0 ]; then - echo "---> Deployment completed!" -else - echo "---> ERROR: Deployment FAILED!" -fi - -exit $RET diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..baaae78 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +import json +import os +from string import Template + +def validate_envs(req_vars): + missing = set(req_vars) - set(os.environ) + if missing: + print('Environment variables not set: %s' % missing) + raise + return True + +def validate_json(json_data): + try: + json.loads(json_data) + return True + except ValueError as err: + print('JSON not valide: %s' % err) + +def json_template(json_template, env_vars=os.environ): + try: + json_file = open(json_template) + data = json_file.read() + except: + print('File %s not found' % json_template) + + try: + template = Template(data).substitute(env_vars) + except KeyError as err: + print('Missing variable %s' % str(err)) + exit(1) + + try: + validate_json(template) + except Exception as err: + print(err) + + return template diff --git a/src/vulnerabilities-check.py b/src/vulnerabilities-check.py deleted file mode 100755 index 2e1bf9a..0000000 --- a/src/vulnerabilities-check.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 - -import os -import boto3 -import json -import sys - -build_version=os.environ['BUILD_VERSION'] -severity=list(os.environ['SEVERITY'].split(' ')) -app_name=os.environ['APP_NAME'] -ecr_account=os.environ['ECR_ACCOUNT'] - -client = boto3.client('ecr') -response = client.describe_image_scan_findings( - registryId=ecr_account, - repositoryName=app_name, - imageId={ - 'imageTag': build_version - }, -) - -print("---> Checking for vulnerabilities") - -if len(response['imageScanFindings']['findings']) == 0: - print("---> No vulnerabilities found") -else: - vuln_counter=0 - for level in severity: - vuln_report=response - if not vuln_report['imageScanFindings']['findings'][0]['severity'] == level: - print("---> The report doesn't have any %s vulnerabilities" %level) - else: - if vuln_report['imageScanFindings']['findingSeverityCounts'][level] > 0: - report = vuln_report['imageScanFindings']['findingSeverityCounts'][level] - print("---> There is/are %s vulnerability(ies) level %s" %(report,level)) - print("---> Packages: %s" %vuln_report['imageScanFindings']['findings'][0]['attributes']) - vuln_counter+=report - - if vuln_counter > 0: - print("---> ERROR: Docker image contains %s vulnerability(ies)" %vuln_counter) - exit(1) - else: - print("---> No vulnerabilities found") diff --git a/src/worker-deploy.py b/src/worker-deploy.py new file mode 100755 index 0000000..7926714 --- /dev/null +++ b/src/worker-deploy.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +import os +import json +import time +from ecs import EcsClient +from utils import validate_envs, json_template + +# ----- Check variables ----- +print('Step 1: Checking environment variables \n') + +req_vars = [ + 'CLUSTER_NAME', + 'APP_NAME', + 'AWS_DEFAULT_REGION' +] + +try: + validate_envs(req_vars) +except: + exit(1) + +cluster_name = os.getenv('CLUSTER_NAME') +app_name = os.getenv('APP_NAME') +aws_default_region = os.getenv('AWS_DEFAULT_REGION') +task_def_file_name = os.getenv('TPL_FILE_NAME', 'task-definition.tpl.json') + +# ----- Create task definition file ----- +print('Step 2: Replace variables inside of %s \n' % task_def_file_name) + +try: + task_definition = json_template(task_def_file_name) +except: + exit(1) + +print('Task definition file: \n%s' % task_definition) +task_def = json.loads(task_definition) + +# ----- Register task definition file ----- +print('Step 3: Registering task definition') +task = EcsClient() + +try: + task.register_task_definition(task_def) + print('Task definition arn: %s \n' % task.taskDefArn) +except Exception as err: + print('Register task definition issue: %s' % err) + exit(1) + +# ----- Create Deployment ----- +print('Step 4: Creating Deployment') + +active_task = task.describe_services(cluster_name, app_name) +active_task_def = active_task['services'][0]['taskDefinition'] + +try: + task.update_service(cluster_name, app_name, task.taskDefArn) +except Exception as err: + print('Deployment FAILED!') + print('ERROR: %s' % str(err).split(": ")[1]) + exit(1) + +deployment = task.describe_services(cluster_name, app_name) +print('ECS dpeloyment: %s \n' % task.ecsDeployId) + +# ----- Monitor Deployment ----- +print('Step 5: Deployment Overview') + +print('Monitoring ECS service events for cluster %s on service %s:\n' % (cluster_name, app_name)) + +ecs_deploy = list(filter(lambda x:x["status"]=="PRIMARY",deployment['services'][0]['deployments'])) +ecs_deploy_status = ecs_deploy[0]['rolloutState'] + +deploy_timeout_period = 0 +deploy_timeout = int(os.getenv('DEPLOYMENT_TIMEOUT', 900)) + +def rollback(): + try: + task.update_service(cluster_name, app_name, active_task_def, True) + print('Rollback deployment success') + except: + print('Rollback deployment failed') + finally: + exit(1) + +while ecs_deploy_status == 'IN_PROGRESS': + # Tail logs from ECS service + ecs_events = task.tail_ecs_events(cluster_name, app_name) + for event in ecs_events: + print('%s %s' % ('{0:%Y-%m-%d %H:%M:%S %z}'.format(event['createdAt']), event['message'])) + + # Check if containers are being stoped + last_task = task.list_tasks(cluster_name, task.ecsDeployId) + if len(last_task['taskArns']) > 2: + last_task_info = task.describe_tasks(cluster_name, last_task['taskArns']) + last_task_status = last_task_info['tasks'][0]['lastStatus'] + last_task_reason = last_task_info['tasks'][0]['stoppedReason'] + if 'reason' in last_task_info['tasks'][0]['containers'][0]: + last_task_reason = '%s \n%s' % ( + last_task_reason, last_task_info['tasks'][0]['containers'][0]['reason']) + + if last_task_status == 'STOPPED': + print('Containers are being stoped: %s' % last_task_reason) + rollback() + + # Rechead limit + if deploy_timeout_period >= deploy_timeout: + print('Deployment timeout: %s seconds' % deploy_timeout) + rollback() + + # Get status, increment limit and sleep + deployment = task.describe_services(cluster_name, app_name) + ecs_deploy = list(filter(lambda x:x["status"]=="PRIMARY",deployment['services'][0]['deployments'])) + ecs_deploy_status = ecs_deploy[0]['rolloutState'] + deploy_timeout_period += 2 + time.sleep(2) + +# Print Status +print('\nDeployment completed:') +print('CLUSTER_NAME: %s' % cluster_name) +print('APP_NAME: %s' % app_name) +print('TASK_ARN: %s' % task.taskDefArn) diff --git a/src/worker-deploy.sh b/src/worker-deploy.sh deleted file mode 100755 index 172fe28..0000000 --- a/src/worker-deploy.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -e - -if [[ ! -f "task-definition.tpl.json" ]]; then - echo "---> ERROR: task-definition.tpl.json not found" - exit 0 -fi - -ERROR=0 -if [[ -z "$AWS_DEFAULT_REGION" ]]; then echo "---> ERROR: Missing variable AWS_DEFAULT_REGION"; ERROR=1; fi -if [[ -z "$APP_NAME" ]]; then echo "---> ERROR: Missing variable APP_NAME"; ERROR=1; fi -if [[ -z "$CLUSTER_NAME" ]]; then echo "---> ERROR: Missing variable CLUSTER_NAME"; ERROR=1; fi -if [[ -z "$IMAGE_NAME" ]]; then echo "---> ERROR: Missing variable IMAGE_NAME"; ERROR=1; fi -if [[ "$ERROR" == "1" ]]; then exit 1; fi - -envsubst < task-definition.tpl.json > task-definition.json -echo "---> Task Definition" -cat task-definition.json - -echo "" -echo "---> Registering Task Definition" - -# Update the ECS service to use the updated Task version - -TASK_ARN=$(aws ecs register-task-definition \ - --cli-input-json file://task-definition.json \ - --query="taskDefinition.taskDefinitionArn" \ - --output=text) - -echo "---> Updating ECS Service" -echo " CLUSTER_NAME: ${CLUSTER_NAME}" -echo " APP_NAME: ${APP_NAME}" -echo " TASK_ARN: ${TASK_ARN}" - -aws ecs update-service \ - --cluster $CLUSTER_NAME \ - --service $APP_NAME \ - --task-definition $TASK_ARN - -RET=$? - -if [ $RET -eq 0 ]; then - echo "---> Deployment completed!" -else - echo "---> ERROR: Deployment FAILED!" -fi - -exit $RET diff --git a/templates/task-definition.tpl-default.json b/templates/task-definition.tpl-default.json new file mode 100644 index 0000000..f9016dc --- /dev/null +++ b/templates/task-definition.tpl-default.json @@ -0,0 +1,32 @@ +{ + "containerDefinitions": [ + { + "essential": true, + "image": "${IMAGE_NAME}", + "command": ${DEFAULT_COMMAND}, + "cpu": ${CPU}, + "memory": ${MEMORY}, + "memoryReservation": ${MEMORY}, + "name": "${APP_NAME}", + "portMappings": [ + { + "containerPort": ${CONTAINER_PORT} + } + ], + "environment": [], + "mountPoints": [], + "volumesFrom": [], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/${CLUSTER_NAME}/${APP_NAME}", + "awslogs-region": "${AWS_DEFAULT_REGION}", + "awslogs-stream-prefix": "${APP_NAME}" + } + } + } + ], + "family": "${CLUSTER_NAME}-${APP_NAME}", + "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecs-task-${CLUSTER_NAME}-${AWS_DEFAULT_REGION}", + "taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecs-task-${CLUSTER_NAME}-${AWS_DEFAULT_REGION}" +} diff --git a/templates/task-definition.tpl-fargate.json b/templates/task-definition.tpl-fargate.json new file mode 100644 index 0000000..202d876 --- /dev/null +++ b/templates/task-definition.tpl-fargate.json @@ -0,0 +1,36 @@ +{ + "containerDefinitions": [ + { + "essential": true, + "image": "${IMAGE_NAME}", + "command": ${DEFAULT_COMMAND}, + "cpu": ${CPU}, + "memory": ${MEMORY}, + "memoryReservation": ${MEMORY}, + "name": "${APP_NAME}", + "portMappings": [ + { + "containerPort": ${CONTAINER_PORT} + } + ], + "environment": [], + "mountPoints": [], + "volumesFrom": [], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/${CLUSTER_NAME}/${APP_NAME}", + "awslogs-region": "${AWS_DEFAULT_REGION}", + "awslogs-stream-prefix": "${APP_NAME}" + } + } + } + ], + "family": "${CLUSTER_NAME}-${APP_NAME}", + "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecs-task-${CLUSTER_NAME}-${AWS_DEFAULT_REGION}", + "taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecs-task-${CLUSTER_NAME}-${AWS_DEFAULT_REGION}", + "networkMode": "awsvpc", + "cpu": "${CPU}", + "memory": "${MEMORY}", + "requiresCompatibilities": [ "FARGATE" ] +} diff --git a/templates/task-definition.tpl-splunk.json b/templates/task-definition.tpl-splunk.json new file mode 100644 index 0000000..85256e7 --- /dev/null +++ b/templates/task-definition.tpl-splunk.json @@ -0,0 +1,35 @@ +{ + "containerDefinitions": [ + { + "essential": true, + "image": "${IMAGE_NAME}", + "command": ${DEFAULT_COMMAND}, + "cpu": ${CPU}, + "memory": ${MEMORY}, + "memoryReservation": ${MEMORY}, + "name": "${APP_NAME}", + "portMappings": [ + { + "containerPort": ${CONTAINER_PORT} + } + ], + "environment": [], + "mountPoints": [], + "volumesFrom": [], + "logConfiguration": { + "logDriver": "splunk", + "options": { + "splunk-token": "placeholder", + "splunk-url": "placeholder" + } + } + } + ], + "family": "${CLUSTER_NAME}-${APP_NAME}", + "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecs-task-${CLUSTER_NAME}-${AWS_DEFAULT_REGION}", + "taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecs-task-${CLUSTER_NAME}-${AWS_DEFAULT_REGION}", + "networkMode": "awsvpc", + "cpu": "${CPU}", + "memory": "${MEMORY}", + "requiresCompatibilities": [ "FARGATE" ] +}