diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d3d7853 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,42 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Build + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + + workflow_call: {} + + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + pip install -r requirements-test.txt + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 ecs_deploy tests --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 ecs_deploy tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest --cov ecs_deploy + run: | + pytest diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..a2756dc --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,47 @@ +name: Docker Hub + +on: + push: + branches: + - 'develop' + - 'master' + tags: + - '*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + + + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: fabfuel + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - + name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: fabfuel/ecs-deploy:${{ github.ref_name }} + - + name: "Build and push (tag: latest)" + if: github.ref == 'refs/heads/develop' + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: fabfuel/ecs-deploy:latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3f9cd6a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Release + +on: + push: + tags: [ '**' ] + +jobs: + build: + uses: ./.github/workflows/build.yml + + release: + needs: + - build + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install tools + run: | + pip install build twine + + - name: Build project + run: python -m build + + - name: Upload to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }} + run: twine upload dist/* diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a0c7b60..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: python -python: - - "2.7" - - "3.5" - - "3.6" - - "3.7" - - "3.8" - - "3.9" -install: pip install tox-travis -script: tox -after_script: - - pip install scrutinizer-ocular - - ocular --data-file ".coverage" diff --git a/Dockerfile b/Dockerfile index a282db7..fa3d669 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-alpine3.13 +FROM python:3.10-alpine ADD . /usr/src/app WORKDIR /usr/src/app diff --git a/README.rst b/README.rst index 43f8fc6..d0204d8 100644 --- a/README.rst +++ b/README.rst @@ -4,13 +4,10 @@ ECS Deploy .. image:: https://badge.fury.io/py/ecs-deploy.svg :target: https://badge.fury.io/py/ecs-deploy -.. image:: https://travis-ci.com/fabfuel/ecs-deploy.svg?branch=develop - :target: https://travis-ci.com/github/fabfuel/ecs-deploy +.. image:: https://github.com/fabfuel/ecs-deploy/actions/workflows/build.yml/badge.svg + :target: https://github.com/fabfuel/ecs-deploy/actions/workflows/build.yml -.. image:: https://scrutinizer-ci.com/g/fabfuel/ecs-deploy/badges/coverage.png?b=develop - :target: https://scrutinizer-ci.com/g/fabfuel/ecs-deploy - -`ecs-deploy` simplifies deployments on Amazon ECS by providing a convinience CLI tool for complex actions, which are executed pretty often. +`ecs-deploy` simplifies deployments on Amazon ECS by providing a convenience CLI tool for complex actions, which are executed pretty often. Key Features ------------ @@ -48,10 +45,13 @@ Update a task definition (without running or deploying):: Installation ------------ -The project is availably on PyPI. Simply run:: +The project is available on PyPI. Simply run:: $ pip install ecs-deploy +For [Homebrew](https://brew.sh/) users, you can also install [it](https://formulae.brew.sh/formula/ecs-deploy) via brew:: + + $ brew install ecs-deploy Run via Docker -------------- @@ -60,7 +60,7 @@ Instead of installing **ecs-deploy** locally, which requires a Python environmen Running **ecs-deploy** via Docker is easy as:: docker run fabfuel/ecs-deploy:1.10.2 - + In this example, the stable version 1.10.2 is executed. Alternatively you can use Docker tags ``master`` or ``latest`` for the latest stable version or Docker tag ``develop`` for the newest development version of **ecs-deploy**. Please be aware, that when running **ecs-deploy** via Docker, the configuration - as described below - does not apply. You have to provide credentials and the AWS region via the command as attributes or environment variables:: @@ -83,7 +83,7 @@ AWS IAM ------- If you are using **ecs-deploy** with a role or user account that does not have full AWS access, such as in a deploy script, you will -need to use or create an IAM policy with the correct set of permissions in order for your deploys to succeed. One option is to use the +need to use or create an IAM policy with the correct set of permissions in order for your deploys to succeed. One option is to use the pre-specified ``AmazonECS_FullAccess`` (https://docs.aws.amazon.com/AmazonECS/latest/userguide/security-iam-awsmanpol.html#security-iam-awsmanpol-AmazonECS_FullAccess) policy. If you would prefer to create a role with a more minimal set of permissions, the following are required: @@ -96,7 +96,7 @@ the following are required: * ``ecs:ListTaskDefinitions`` * ``ecs:DescribeTaskDefinition`` * ``ecs:DeregisterTaskDefinition`` - + If using custom IAM permissions, you will also need to set the ``iam:PassRole`` policy for each IAM role. See here https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_passrole.html for more information. Note that not every permission is required for every action you can take in **ecs-deploy**. You may be able to adjust permissions based on your specific needs. @@ -146,7 +146,7 @@ Deployment Simple Redeploy =============== -To redeploy a service without any modifications, but pulling the most recent image versions, run the follwing command. +To redeploy a service without any modifications, but pulling the most recent image versions, run the following command. This will duplicate the current task definition and cause the service to redeploy all running tasks.:: $ ecs deploy my-cluster my-service @@ -261,6 +261,22 @@ Instead of setting environment variables separately, you can pass a .env file pe $ ecs deploy my-cluster my-service --s3-env-file my-app arn:aws:s3:::my-ecs-environment/my-app.env +Set secrets via .env files +============================== +Instead of setting secrets separately, you can pass a .env file per container to set all secrets at once. + +This will expect an env file format, but any values will be set as the `valueFrom` parameter in the secrets config. +This value can be either the path or the full ARN of a secret in the AWS Parameter Store. For example, with a secrets.env +file like the following: + +``` +SOME_SECRET=arn:aws:ssm:::parameter/KEY_OF_SECRET_IN_PARAMETER_STORE +``` + +$ ecs deploy my-cluster my-service --secret-env-file webserver env/secrets.env + +This will modify the **webserver** container definition and add or overwrite the environment variable `SOME_SECRET` with the value of the `KEY_OF_SECRET_IN_PARAMETER_STORE` in the AWS Parameter Store of the AWS Systems Manager. + Set a docker label =================== @@ -297,8 +313,8 @@ To change the command of a specific container, run the following command:: $ ecs deploy my-cluster my-service --command webserver "nginx" -This will modify the **webserver** container and change its command to "nginx". If you have -a command that requries arugments as well, then you can simply specify it like this as you would normally do: +This will modify the **webserver** container and change its command to "nginx". If you have +a command that requires arguments as well, then you can simply specify it like this as you would normally do: $ ecs deploy my-cluster my-service --command webserver "ngnix -c /etc/ngnix/ngnix.conf" @@ -324,13 +340,15 @@ This will set the task role to "MySpecialEcsTaskRole". Set CPU and memory reservation ============================== -- Set the `cpu` value for a task definition: :code:`--cpu 0`. -- Set the `memory` value (`hard limit`) for a task definition: :code:`--memory 256`. +- Set the `cpu` value for a task: :code:`--task-cpu 0`. +- Set the `cpu` value for a task container: :code:`--cpu 0`. +- Set the `memory` value (`hard limit`) for a task: :code:`--task-memory 256`. +- Set the `memory` value (`hard limit`) for a task container: :code:`--memory 256`. - Set the `memoryreservation` value (`soft limit`) for a task definition: :code:`--memoryreservation 256`. Set privileged or essential flags ================================= -- Set the `privliged` value for a task definition: :code:`--privileged True|False`. +- Set the `privileged` value for a task definition: :code:`--privileged True|False`. - Set the `essential` value for a task definition: :code:`--essential True|False`. Set logging configuration @@ -377,7 +395,7 @@ Placeholder Container ===================== - Add placeholder containers: :code:`--add-container `. - To comply with the minimum requirements for a task definition, a placeholder container is set like this: - + The contaienr name is :code:``. + + The container name is :code:``. + The container image is :code:`PLACEHOLDER`. + The container soft limit is :code:`128`. - The idea is to set sensible values with the deployment. @@ -423,6 +441,21 @@ This instructs ecs-deploy to wait for ECS to finish the deployment for the given To run a deployment without waiting for the successful or failed result at all, set ``--timeout`` to the value of ``-1``. + +Multi-Account Setup +=================== +If you manage different environments of your system in multiple differnt AWS accounts, you can now easily assume a +deployment role in the target account in which your ECS cluster is running. You only need to provide ``--account`` +with the AWS account id and ``--assume-role`` with the name of the role you want to assume in the target account. +ecs-deploy automatically assumes this role and deploys inside your target account: + +Example:: + + $ ecs deploy my-cluster my-service --account 1234567890 --assume-role ecsDeployRole + + + + Scaling ------- @@ -500,7 +533,7 @@ Optionally you can provide additional information for the deployment: - ``--comment "New feature X"`` - comment to the deployment - ``--user john.doe`` - the name of the user who deployed with -- ``--newrelic-revision 1.0.0`` - explicitly set the revison to use for the deployment +- ``--newrelic-revision 1.0.0`` - explicitly set the revision to use for the deployment Note: If neither ``--tag`` nor ``--newrelic-revision`` are provided, the deployment will not be recorded. diff --git a/ecs_deploy/__init__.py b/ecs_deploy/__init__.py index f192018..248bfdd 100644 --- a/ecs_deploy/__init__.py +++ b/ecs_deploy/__init__.py @@ -1 +1 @@ -VERSION = '1.12.1' +VERSION = '1.15.2' diff --git a/ecs_deploy/cli.py b/ecs_deploy/cli.py index d60a24a..679fe93 100644 --- a/ecs_deploy/cli.py +++ b/ecs_deploy/cli.py @@ -7,6 +7,7 @@ import json import getpass from datetime import datetime, timedelta +from botocore.exceptions import ClientError from ecs_deploy import VERSION from ecs_deploy.ecs import DeployAction, ScaleAction, RunAction, EcsClient, DiffAction, \ TaskPlacementError, EcsError, UpdateAction, LAUNCH_TYPE_EC2, LAUNCH_TYPE_FARGATE @@ -20,8 +21,8 @@ def ecs(): # pragma: no cover pass -def get_client(access_key_id, secret_access_key, region, profile): - return EcsClient(access_key_id, secret_access_key, region, profile) +def get_client(access_key_id, secret_access_key, region, profile, assume_account, assume_role): + return EcsClient(access_key_id, secret_access_key, region, profile, assume_account=assume_account, assume_role=assume_role) @click.command() @@ -34,12 +35,15 @@ def get_client(access_key_id, secret_access_key, region, profile): @click.option('--cpu', type=(str, int), multiple=True, help='Overwrites the cpu value for a container: ') @click.option('--memory', type=(str, int), multiple=True, help='Overwrites the memory value for a container: ') @click.option('--memoryreservation', type=(str, int), multiple=True, help='Overwrites the memory reservation value for a container: ') +@click.option('--task-cpu', type=int, help='Overwrites the cpu value for a task: ') +@click.option('--task-memory', type=int, help='Overwrites the memory value for a task: ') @click.option('--privileged', type=(str, bool), multiple=True, help='Overwrites the privileged value for a container: ') @click.option('--essential', type=(str, bool), multiple=True, help='Overwrites the essential value for a container: ') @click.option('-e', '--env', type=(str, str, str), multiple=True, help='Adds or changes an environment variable: ') @click.option('--env-file', type=(str, str), default=((None, None),), multiple=True, required=False, help='Load environment variables from .env-file: ') @click.option('--s3-env-file', type=(str, str), multiple=True, required=False, help='Location of .env-file in S3 in ARN format (eg arn:aws:s3:::/bucket_name/object_name): ') @click.option('-s', '--secret', type=(str, str, str), multiple=True, help='Adds or changes a secret environment variable from the AWS Parameter Store (Not available for Fargate): ') +@click.option('--secrets-env-file', type=(str, str), default=((None, None),), multiple=True, required=False, help='Load secrets from .env-file: ') @click.option('-d', '--docker-label', type=(str, str, str), multiple=True, help='Adds or changes a docker label: ') @click.option('-u', '--ulimit', type=(str, str, int, int), multiple=True, help='Adds or changes a ulimit variable in the container description (Not available for Fargate): ') @click.option('--system-control', type=(str, str, str), multiple=True, help='Adds or changes a system control variable in the container description (Not available for Fargate): ') @@ -48,11 +52,14 @@ def get_client(access_key_id, secret_access_key, region, profile): @click.option('-l', '--log', type=(str, str, str, str), multiple=True, help='Adds or changes a log configuration in the container description (Not available for Fargate):