├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── index.py ├── infrastructure ├── ecr.tf ├── ecs.tf ├── iam.tf ├── main.tf ├── task-def │ └── def.json ├── terraform.tfvars └── vars.tf ├── push.sh └── run.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | infrastructure/ 2 | .git/ 3 | .idea/ 4 | *.iml 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | 4 | *.tfstate 5 | *.tfstate.backup 6 | .terraform/ 7 | 8 | repo 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | RUN pip install requests 3 | WORKDIR /app 4 | ADD index.py /app/ 5 | CMD [ "python", "/app/index.py" ] 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Running ECS task on a schedule 2 | 3 | ## File description 4 | 5 | * `infrastructure/ecs.tf` - ECS cluster and task configuration 6 | * `infrastrcuture/ecr.tf` - image repository configuration 7 | 8 | ## Shortcuts and scripts 9 | 10 | 1. Run docker locally: 11 | ```shell script 12 | $ ./run.sh 13 | ``` 14 | 15 | 2. Publish docker to your ECR repository: 16 | ```shell script 17 | $ echo "YOUR_ECR_REPOSITORY_URI" > repo 18 | $ ./push.sh 19 | ``` 20 | 21 | 3. Apply terraform changes to your environment: 22 | ```shell script 23 | $ cd infrastructure/ 24 | $ terraform apply 25 | ``` 26 | 27 | ## AWS Access Configuration 28 | 29 | For terraform and docker push scripts to find your AWS configuration, place the configuration in the `~/.aws/scheduled-ecs-credentials` file: 30 | 31 | ``` 32 | [default] 33 | aws_access_key_id=YOUR_ACCESS_KEY 34 | aws_secret_access_key=YOUR_SECRET_ACCESS_KEY 35 | region=us-west-2 36 | ``` 37 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | import requests, sys 2 | 3 | print("Calling httpbin") 4 | 5 | r = requests.get('https://httpbin.org/get') 6 | if r.status_code != 200: 7 | print("Failed") 8 | sys.exit(1) 9 | 10 | print("Successfully completed") 11 | 12 | 13 | -------------------------------------------------------------------------------- /infrastructure/ecr.tf: -------------------------------------------------------------------------------- 1 | data "aws_ecr_repository" "ecr_repository" { 2 | name = local.service_name 3 | } 4 | 5 | data "aws_ecr_image" "ecr_image" { 6 | repository_name = data.aws_ecr_repository.ecr_repository.name 7 | image_tag = "latest" 8 | } 9 | -------------------------------------------------------------------------------- /infrastructure/ecs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_cluster" "cluster" { 2 | name = "${local.service_name}-cluster" 3 | } 4 | 5 | resource "aws_cloudwatch_log_group" "task_log_group" { 6 | retention_in_days = 1 7 | name_prefix = "${local.service_name}-" 8 | } 9 | 10 | data "template_file" "definition" { 11 | template = file("${path.module}/task-def/def.json") 12 | vars = { 13 | region = var.region 14 | log_group = aws_cloudwatch_log_group.task_log_group.name 15 | image_tag = data.aws_ecr_repository.ecr_repository.repository_url 16 | definition_name = local.service_name 17 | } 18 | } 19 | 20 | resource "aws_ecs_task_definition" "definition" { 21 | family = local.service_name 22 | container_definitions = data.template_file.definition.rendered 23 | task_role_arn = aws_iam_role.task_role.arn 24 | execution_role_arn = aws_iam_role.service_role.arn 25 | requires_compatibilities = ["FARGATE"] 26 | network_mode = "awsvpc" 27 | cpu = "512" 28 | memory = "1024" 29 | } 30 | 31 | resource "aws_ecs_service" "service" { 32 | name = "${local.service_name}-service" 33 | task_definition = aws_ecs_task_definition.definition.arn 34 | cluster = aws_ecs_cluster.cluster.id 35 | deployment_maximum_percent = 200 36 | deployment_minimum_healthy_percent = 100 37 | desired_count = 0 38 | launch_type = "FARGATE" 39 | network_configuration { 40 | subnets = data.aws_subnet_ids.subnets.ids 41 | assign_public_ip = true 42 | security_groups = [aws_security_group.ecs.id] 43 | } 44 | } 45 | 46 | 47 | resource "aws_security_group" "ecs" { 48 | vpc_id = aws_default_vpc.default.id 49 | egress { 50 | protocol = "-1" 51 | from_port = 0 52 | to_port = 0 53 | cidr_blocks = ["0.0.0.0/0"] 54 | description = "All outbound traffic" 55 | } 56 | } 57 | 58 | resource "aws_cloudwatch_event_rule" "scheduled_task" { 59 | name = "${local.service_name}-event-rule" 60 | schedule_expression = "rate(5 minutes)" // https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions 61 | is_enabled = false // Change to true to enable scheduling 62 | } 63 | 64 | resource "aws_cloudwatch_event_target" "scheduled_task" { 65 | target_id = "${local.service_name}-target" 66 | rule = aws_cloudwatch_event_rule.scheduled_task.name 67 | arn = aws_ecs_cluster.cluster.arn 68 | role_arn = aws_iam_role.scheduled_task_cloudwatch.arn 69 | 70 | ecs_target { 71 | task_count = 1 72 | task_definition_arn = aws_ecs_task_definition.definition.arn 73 | launch_type = "FARGATE" 74 | network_configuration { 75 | subnets = data.aws_subnet_ids.subnets.ids 76 | assign_public_ip = true 77 | security_groups = [aws_security_group.ecs.id] 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /infrastructure/iam.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | resource "aws_iam_role" "service_role" { 4 | name_prefix = "service_role_" 5 | assume_role_policy = <