# 167 Technical 6dc7aeaf

> AWS CloudFormation Deploy - Technical Reference

- Skill: `tools-only/167-technical-6dc7aeaf` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/167-technical-6dc7aeaf`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/167-technical-6dc7aeaf/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/tools-only/167-technical-6dc7aeaf

---

# AWS CloudFormation Deploy - Technical Reference

Comprehensive technical reference for deploying ECS containers with GitHub Actions and CloudFormation.

## Table of Contents

- [GitHub Actions Workflow Syntax](#github-actions-workflow-syntax)
- [AWS OIDC Trust Policy Configuration](#aws-oidc-trust-policy-configuration)
- [ECR Repository Configuration](#ecr-repository-configuration)
- [Task Definition Structure](#task-definition-structure)
- [ECS Service Update Parameters](#ecs-service-update-parameters)
- [CloudFormation Stack Updates](#cloudformation-stack-updates)
- [Security Considerations](#security-considerations)
- [Environment Configurations](#environment-configurations)
- [Monitoring and Logging](#monitoring-and-logging)

## GitHub Actions Workflow Syntax

### Workflow File Structure

GitHub Actions workflows are defined in YAML files in `.github/workflows/`:

```yaml
name: Workflow Name                    # Human-readable name
on:                                    # Triggers for workflow
  push:                                # Run on git push
    branches: [main, develop]          # Specific branches
    paths:                             # Specific file paths
      - 'src/**'
      - 'Dockerfile'
  pull_request:                        # Run on pull requests
  schedule:                            # Cron-based triggers
    - cron: '0 0 * * *'
  workflow_dispatch:                   # Manual trigger
  release:                             # Run on release
    types: [created]

env:                                   # Environment variables available to all jobs
  AWS_REGION: us-east-1
  ECR_REPOSITORY: my-app

permissions:                           # Workflow-level permissions
  id-token: write                      # Required for OIDC
  contents: read                       # Required for checkout
  pull-requests: write                 # For PR comments

jobs:                                  # Job definitions
  job-name:
    runs-on: ubuntu-latest             # Runner type
    timeout-minutes: 30                # Job timeout
    permissions:                       # Job-specific permissions (overrides workflow-level)
      id-token: write
    steps:                             # Step sequence
      - name: Step name
        uses: action@v1                # Use an action
      - name: Run command
        run: |                         # Run shell commands
          echo "Command"
```

### Workflow Triggers

**Push-based triggers:**

```yaml
on:
  push:
    branches:
      - main
      - 'release/**'                  # Wildcard matching
    tags:
      - 'v*.*.*'                      # Semantic versioning
    paths:
      - 'src/**'
      - 'Dockerfile'
      - '.github/workflows/deploy.yml'
```

**Pull request triggers:**

```yaml
on:
  pull_request:
    types: [opened, synchronize, reopened]
    branches:
      - main
      - develop
```

**Manual trigger with inputs:**

```yaml
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        type: choice
        options:
          - dev
          - staging
          - prod
      version:
        description: 'Image version tag'
        required: false
        default: 'latest'
```

**Scheduled triggers:**

```yaml
on:
  schedule:
    - cron: '0 2 * * *'               # Daily at 2 AM UTC
    - cron: '0 0 * * 0'               # Weekly on Sunday
```

### Job Dependencies and Matrix Builds

**Sequential jobs:**

```yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  build:
    needs: test                       # Requires test job to complete
    runs-on: ubuntu-latest
    steps:
      - run: docker build
```

**Parallel matrix builds:**

```yaml
jobs:
  deploy:
    strategy:
      matrix:
        environment: [dev, staging, prod]
        region: [us-east-1, eu-west-1]
        include:
          - environment: prod
            region: us-east-1
            timeout: 60
        exclude:
          - environment: prod
            region: eu-west-1
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Deploy to ${{ matrix.environment }} in ${{ matrix.region }}"
```

**Conditional execution:**

```yaml
jobs:
  deploy:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Only runs on main branch push"
```

### Step Syntax and Features

**Using actions:**

```yaml
steps:
  - name: Checkout code
    uses: actions/checkout@v4
    with:
      fetch-depth: 0                  # Full git history
      token: ${{ secrets.GITHUB_TOKEN }}
      sparse-checkout: |
        src/
        Dockerfile

  - name: Configure AWS
    uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/my-role
      aws-region: us-east-1
      mask-aws-account-id: true       # Hide account ID in logs
```

**Running shell commands:**

```yaml
steps:
  - name: Build and push
    run: |
      echo "Building Docker image..."
      docker build -t my-app:${{ github.sha }} .
      docker push my-app:${{ github.sha }}
    shell: bash                       # Optional: bash, pwsh, python, etc.

  - name: Multi-line script
    run: |
      set -x                          # Debug mode
      aws ecr describe-repositories \
        --repository-names my-app \
        --region $AWS_REGION
```

**Working with outputs:**

```yaml
steps:
  - id: login-ecr
    name: Login to ECR
    uses: aws-actions/amazon-ecr-login@v2

  - name: Use output
    run: |
      echo "Registry: ${{ steps.login-ecr.outputs.registry }}"
      echo "Using registry URL in next step..."
```

**Conditional steps:**

```yaml
steps:
  - name: Deploy to production
    if: github.ref == 'refs/heads/main'
    run: |
      echo "Deploying to production"

  - name: Deploy to staging
    if: github.ref == 'refs/heads/staging'
    run: |
      echo "Deploying to staging"
```

### Environment Variables and Secrets

**Setting environment variables:**

```yaml
env:
  GLOBAL_VAR: value
jobs:
  job1:
    env:
      JOB_VAR: value
    steps:
      - env:
          STEP_VAR: value
        run: |
          echo "$GLOBAL_VAR $JOB_VAR $STEP_VAR"
```

**Using secrets:**

```yaml
steps:
  - name: Use secret
    env:
      SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
    run: |
      curl -X POST $SLACK_WEBHOOK -d "Deployment complete"
```

**Passing outputs between jobs:**

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.build.outputs.tag }}
    steps:
      - id: build
        run: echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Using image tag: ${{ needs.build.outputs.image-tag }}"
```

## AWS OIDC Trust Policy Configuration

### OIDC Provider Setup

**Create OIDC provider in AWS IAM:**

```bash
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com
```

**Or via CloudFormation:**

```yaml
GitHubOIDCProvider:
  Type: AWS::IAM::OIDCProvider
  Properties:
    Url: https://token.actions.githubusercontent.com
    ClientIdList:
      - sts.amazonaws.com
    ThumbprintList:
      - a031c46782e6e6c662c2c87c76da9aa62ccabd8e
```

### IAM Role with OIDC Trust Policy

**Basic OIDC trust policy:**

```yaml
GitHubActionsRole:
  Type: AWS::IAM::Role
  Properties:
    RoleName: github-actions-ecs-role
    Description: Role for GitHub Actions to deploy ECS
    AssumeRolePolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Principal:
            Federated: !Sub 'arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com'
          Action: sts:AssumeRoleWithWebIdentity
          Condition:
            StringEquals:
              token.actions.githubusercontent.com:aud: sts.amazonaws.com
```

**Repository-scoped trust policy:**

```yaml
AssumeRolePolicyDocument:
  Version: '2012-10-17'
  Statement:
    - Effect: Allow
      Principal:
        Federated: !Sub 'arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com'
      Action: sts:AssumeRoleWithWebIdentity
      Condition:
        StringEquals:
          token.actions.githubusercontent.com:aud: sts.amazonaws.com
          token.actions.githubusercontent.com:sub: repo:my-org/my-repo:ref:refs/heads/main
```

**Environment-based trust policy:**

```yaml
AssumeRolePolicyDocument:
  Version: '2012-10-17'
  Statement:
    - Effect: Allow
      Principal:
        Federated: !Sub 'arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com'
      Action: sts:AssumeRoleWithWebIdentity
      Condition:
        StringEquals:
          token.actions.githubusercontent.com:aud: sts.amazonaws.com
        StringLike:
          token.actions.githubusercontent.com:sub:
            - repo:my-org/my-repo:ref:refs/heads/main
            - repo:my-org/my-repo:ref:refs/heads/production
```

**Multi-repository trust policy:**

```yaml
AssumeRolePolicyDocument:
  Version: '2012-10-17'
  Statement:
    - Effect: Allow
      Principal:
        Federated: !Sub 'arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com'
      Action: sts:AssumeRoleWithWebIdentity
      Condition:
        StringEquals:
          token.actions.githubusercontent.com:aud: sts.amazonaws.com
        StringLike:
          token.actions.githubusercontent.com:sub:
            - repo:my-org/*:ref:refs/heads/main
            - repo:my-org/*:ref:refs/heads/production
```

### Role Permissions for ECS Deployment

**ECS-specific permissions:**

```yaml
ECSDeployPolicy:
  Type: AWS::IAM::Policy
  Properties:
    PolicyName: ECSDeployPolicy
    PolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Action:
            - ecs:DescribeServices
            - ecs:DescribeTaskDefinition
            - ecs:DescribeTasks
            - ecs:ListTasks
            - ecs:ListServices
            - ecs:ListClusters
            - ecs:RegisterTaskDefinition
            - ecs:UpdateService
            - ecs:CreateService
            - ecs:DeleteService
            - ecs:UpdateServicePrimaryTaskSet
            - ecs:CreateTaskSet
          Resource:
            - !Sub 'arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*'
```

**ECR-specific permissions:**

```yaml
ECRPushPolicy:
  Type: AWS::IAM::Policy
  Properties:
    PolicyName: ECRPushPolicy
    PolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Action:
            - ecr:GetAuthorizationToken
          Resource: '*'
        - Effect: Allow
          Action:
            - ecr:BatchCheckLayerAvailability
            - ecr:GetDownloadUrlForLayer
            - ecr:GetRepositoryPolicy
            - ecr:DescribeRepositories
            - ecr:ListImages
            - ecr:DescribeImages
            - ecr:BatchGetImage
            - ecr:InitiateLayerUpload
            - ecr:UploadLayerPart
            - ecr:CompleteLayerUpload
            - ecr:PutImage
            - ecr:SetRepositoryPolicy
          Resource:
            - !Sub 'arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/my-app'
```

**CloudFormation permissions:**

```yaml
CloudFormationDeployPolicy:
  Type: AWS::IAM::Policy
  Properties:
    PolicyName: CloudFormationDeployPolicy
    PolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Action:
            - cloudformation:DescribeStacks
            - cloudformation:DescribeStackEvents
            - cloudformation:DescribeStackResource
            - cloudformation:CreateStack
            - cloudformation:UpdateStack
            - cloudformation:DeleteStack
            - cloudformation:ListStacks
            - cloudformation:GetTemplate
            - cloudformation:ValidateTemplate
          Resource:
            - !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/my-app-*'
        - Effect: Allow
          Action:
            - iam:PassRole
          Resource:
            - !Sub 'arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole'
            - !Sub 'arn:aws:iam::${AWS::AccountId}:role/ecsTaskRole'
          Condition:
            StringEquals:
              iam:PassedToService: ecs-tasks.amazonaws.com
```

**Secrets Manager permissions:**

```yaml
SecretsAccessPolicy:
  Type: AWS::IAM::Policy
  Properties:
    PolicyName: SecretsAccessPolicy
    PolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Action:
            - secretsmanager:GetSecretValue
            - secretsmanager:DescribeSecret
          Resource:
            - !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:my-app/*'
```

### Session Tags and Transitive Keys

**Enable session tags for cross-account access:**

```yaml
AssumeRolePolicyDocument:
  Version: '2012-10-17'
  Statement:
    - Effect: Allow
      Principal:
        Federated: !Sub 'arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com'
      Action: sts:AssumeRoleWithWebIdentity
      Condition:
        StringEquals:
          token.actions.githubusercontent.com:aud: sts.amazonaws.com
        StringLike:
          token.actions.githubusercontent.com:sub: repo:my-org/my-repo:*
        ForAllValues:StringEquals:
          aws:PrincipalTag/JobBranch: [main, production]
```

**Configure session tags in GitHub Actions:**

```yaml
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecs-role
    aws-region: us-east-1
    audience: sts.amazonaws.com
```

## ECR Repository Configuration

### Basic ECR Repository

```yaml
ECRRepository:
  Type: AWS::ECR::Repository
  Properties:
    RepositoryName: my-app
    ImageScanningConfiguration:
      ScanOnPush: true
    ImageTagMutability: IMMUTABLE
    EncryptionConfiguration:
      EncryptionType: KMS
      KmsKey: !GetAtt KMSKey.Arn
```

### Lifecycle Policy

**Keep last N tagged images:**

```yaml
LifecyclePolicy:
  Type: AWS::ECR::LifecyclePolicy
  Properties:
    RepositoryName: !Ref ECRRepository
    LifecyclePolicyText: |
      {
        "rules": [
          {
            "rulePriority": 1,
            "description": "Keep last 30 tagged images",
            "selection": {
              "tagStatus": "tagged",
              "tagPrefixList": ["v"],
              "countType": "imageCountMoreThan",
              "countNumber": 30
            },
            "action": {
              "type": "expire"
            }
          }
        ]
      }
```

**Clean up untagged images:**

```yaml
LifecyclePolicy:
  Type: AWS::ECR::LifecyclePolicy
  Properties:
    RepositoryName: !Ref ECRRepository
    LifecyclePolicyText: |
      {
        "rules": [
          {
            "rulePriority": 1,
            "description": "Expire untagged images older than 7 days",
            "selection": {
              "tagStatus": "untagged",
              "countType": "sinceImagePushed",
              "countUnit": "days",
              "countNumber": 7
            },
            "action": {
              "type": "expire"
            }
          }
        ]
      }
```

**Multiple lifecycle rules:**

```yaml
LifecyclePolicy:
  Type: AWS::ECR::LifecyclePolicy
  Properties:
    RepositoryName: !Ref ECRRepository
    LifecyclePolicyText: |
      {
        "rules": [
          {
            "rulePriority": 1,
            "description": "Keep production tagged images",
            "selection": {
              "tagStatus": "tagged",
              "tagPrefixList": ["prod"],
              "countType": "imageCountMoreThan",
              "countNumber": 10
            },
            "action": {
              "type": "expire"
            }
          },
          {
            "rulePriority": 2,
            "description": "Keep last 30 development images",
            "selection": {
              "tagStatus": "tagged",
              "tagPrefixList": ["dev", "staging"],
              "countType": "imageCountMoreThan",
              "countNumber": 30
            },
            "action": {
              "type": "expire"
            }
          },
          {
            "rulePriority": 3,
            "description": "Expire untagged images after 1 day",
            "selection": {
              "tagStatus": "untagged",
              "countType": "sinceImagePushed",
              "countUnit": "days",
              "countNumber": 1
            },
            "action": {
              "type": "expire"
            }
          }
        ]
      }
```

### Repository Policy

**Cross-account access:**

```yaml
RepositoryPolicy:
  Type: AWS::ECR::RepositoryPolicy
  Properties:
    RepositoryName: !Ref ECRRepository
    PolicyText: |
      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Sid": "AllowCrossAccountPull",
            "Effect": "Allow",
            "Principal": {
              "AWS": [
                "arn:aws:iam::111111111111:root",
                "arn:aws:iam::222222222222:root"
              ]
            },
            "Action": [
              "ecr:GetDownloadUrlForLayer",
              "ecr:BatchGetImage",
              "ecr:BatchCheckLayerAvailability"
            ]
          }
        ]
      }
```

**Pull-only access for specific IAM role:**

```yaml
RepositoryPolicy:
  Type: AWS::ECR::RepositoryPolicy
  Properties:
    RepositoryName: !Ref ECRRepository
    PolicyText: |
      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Sid": "AllowECSPull",
            "Effect": "Allow",
            "Principal": {
              "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": [
              "ecr:GetDownloadUrlForLayer",
              "ecr:BatchGetImage",
              "ecr:BatchCheckLayerAvailability"
            ],
            "Condition": {
              "ArnLike": {
                "aws:SourceArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/*"
              }
            }
          }
        ]
      }
```

### ECR Registry Scanning Configuration

**Basic scan on push:**

```yaml
ECRRepository:
  Type: AWS::ECR::Repository
  Properties:
    RepositoryName: my-app
    ImageScanningConfiguration:
      ScanOnPush: true
```

**Enhanced scanning with continuous monitoring:**

```yaml
# First create an inspector scanner
InspectorScanner:
  Type: AWS::InspectorV2::Filter
  Properties:
    Name: ECRScanFilter
    FilterCriteria:
      -
    # Configure scanning rules

ECRRepository:
  Type: AWS::ECR::Repository
  Properties:
    RepositoryName: my-app
    ImageScanningConfiguration:
      ScanOnPush: true
    ImageTagMutability: IMMUTABLE
```

### ECR Event Notifications

**CloudWatch Events for image push:**

```yaml
ImagePushEventRule:
  Type: AWS::Events::Rule
  Properties:
    Name: ECRImagePush
    Description: Trigger on image push
    EventPattern:
      source:
        - aws.ecr
      detail-type:
        - ECR Image Action
      detail:
        action-type:
          - PUSH
    State: ENABLED
    Targets:
      - Arn: !GetAtt LambdaFunction.Arn
        Id: ECRPushTarget

# Lambda function to trigger deployment
LambdaPermission:
  Type: AWS::Lambda::Permission
  Properties:
    FunctionName: !Ref LambdaFunction
    Action: lambda:InvokeFunction
    Principal: events.amazonaws.com
    SourceArn: !GetAtt ImagePushEventRule.Arn
```

## Task Definition Structure

### Basic Fargate Task Definition

```yaml
TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties:
    Family: my-app-task
    Cpu: '256'
    Memory: '512'
    NetworkMode: awsvpc
    RequiresCompatibilities:
      - FARGATE
    ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
    TaskRoleArn: !GetAtt TaskRole.Arn
    ContainerDefinitions:
      - Name: my-app
        Image: !Ref ImageUrl
        Essential: true
        PortMappings:
          - ContainerPort: 8080
            Protocol: tcp
        Environment:
          - Name: ENVIRONMENT
            Value: !Ref Environment
          - Name: LOG_LEVEL
            Value: INFO
        Secrets:
          - Name: DB_PASSWORD
            ValueFrom: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:my-app/db-password'
        LogConfiguration:
          LogDriver: awslogs
          Options:
            awslogs-group: !Ref LogGroup
            awslogs-region: !Ref AWS::Region
            awslogs-stream-prefix: ecs
            awslogs-create-group: 'true'
        HealthCheck:
          Command:
            - CMD-SHELL
            - curl -f http://localhost:8080/actuator/health || exit 1
          Interval: 30
          Timeout: 5
          Retries: 3
          StartPeriod: 60
        DockerLabels:
          com.mycompany.label: value
        ReadonlyRootFilesystem: false
        Ulimits:
          - Name: nofile
            SoftLimit: 65536
            HardLimit: 65536
```

### Multi-Container Task Definition

```yaml
TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties:
    Family: multi-container-app
    Cpu: '512'
    Memory: '1024'
    NetworkMode: awsvpc
    RequiresCompatibilities:
      - FARGATE
    ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
    ContainerDefinitions:
      # Application container
      - Name: app
        Image: !Ref AppImageUrl
        Essential: true
        PortMappings:
          - ContainerPort: 8080
            Protocol: tcp
        Environment:
          - Name: DATABASE_HOST
            Value: localhost
          - Name: DATABASE_PORT
            Value: '5432'
        DependsOn:
          - ContainerName: database
            Condition: START
        LogConfiguration:
          LogDriver: awslogs
          Options:
            awslogs-group: !Ref LogGroup
            awslogs-region: !Ref AWS::Region
            awslogs-stream-prefix: app

      # Sidecar container (e.g., log collector)
      - Name: log-collector
        Image: !Ref LogCollectorImageUrl
        Essential: false
        Environment:
          - Name: LOG_PATH
            Value: /var/log/app
        MountPoints:
          - SourceVolume: app-logs
            ContainerPath: /var/log/app
            ReadOnly: false
        LogConfiguration:
          LogDriver: awslogs
          Options:
            awslogs-group: !Ref LogGroup
            awslogs-region: !Ref AWS::Region
            awslogs-stream-prefix: log-collector

      # Database container (for development)
      - Name: database
        Image: postgres:14
        Essential: false
        PortMappings:
          - ContainerPort: 5432
            Protocol: tcp
        Environment:
          - Name: POSTGRES_DB
            Value: myapp
          - Name: POSTGRES_USER
            Value: appuser
          - Name: POSTGRES_PASSWORD
            Value: secret123
        MountPoints:
          - SourceVolume: db-data
            ContainerPath: /var/lib/postgresql/data
            ReadOnly: false
        LogConfiguration:
          LogDriver: awslogs
          Options:
            awslogs-group: !Ref LogGroup
            awslogs-region: !Ref AWS::Region
            awslogs-stream-prefix: database

    Volumes:
      - Name: app-logs
      - Name: db-data
```

### Firelens Configuration for Log Routing

```yaml
TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties:
    Family: my-app-firelens
    Cpu: '512'
    Memory: '1024'
    NetworkMode: awsvpc
    RequiresCompatibilities:
      - FARGATE
    ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
    RuntimePlatform:
      CpuArchitecture: X86_64
      OperatingSystemFamily: LINUX
    ContainerDefinitions:
      # Firelens sidecar
      - Name: log_router
        Image: public.ecr.aws/aws-observability/aws-for-fluent-bit:stable
        Essential: true
        ReadOnlyRootFilesystem: true
        FirelensConfiguration:
          Type: fluentbit
          Options:
            Enable-CloudWatchLogs: 'true'
            Enable-ELBLogs: 'true'
            ConfigFileValueType: file
        LogConfiguration:
          LogDriver: awslogs
          Options:
            awslogs-group: !Ref FirelensLogGroup
            awslogs-region: !Ref AWS::Region
            awslogs-stream-prefix: firelens
            awslogs-create-group: 'true'

      # Application container using Firelens
      - Name: my-app
        Image: !Ref ImageUrl
        Essential: true
        LogConfiguration:
          LogDriver: awsfirelens
          Options:
            Name: cloudwatch_logs
            region: !Ref AWS::Region
            log_key: log
            log_group_name: !Ref AppLogGroup
            auto_create_group: 'true'
        PortMappings:
          - ContainerPort: 8080
```

### Environment Variable Injection Strategies

**Static environment variables:**

```yaml
Environment:
  - Name: ENVIRONMENT
    Value: production
  - Name: LOG_LEVEL
    Value: INFO
  - Name: MAX_THREADS
    Value: '200'
```

**Parameter Store references:**

```yaml
Environment:
  - Name: API_ENDPOINT
    Value: !Sub '{{resolve:ssm:/my-app/${Environment}/api-endpoint:1}}'
  - Name: MAX_CONNECTIONS
    Value: !Sub '{{resolve:ssm:/my-app/${Environment}/max-connections:1}}'
```

**Secrets Manager references:**

```yaml
Secrets:
  - Name: DATABASE_PASSWORD
    ValueFrom: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:my-app/database-password'
  - Name: API_KEY
    ValueFrom: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:my-app/api-key'
```

**Dynamic image URL from CloudFormation parameter:**

```yaml
Parameters:
  ImageUrl:
    Type: String
    Description: Docker image URL from ECR

Resources:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Name: my-app
          Image: !Ref ImageUrl
```

## ECS Service Update Parameters

### Service Update Configuration

```yaml
ECSService:
  Type: AWS::ECS::Service
  Properties:
    ServiceName: my-service
    Cluster: !Ref ECSCluster
    TaskDefinition: !Ref TaskDefinition
    DeploymentConfiguration:
      MaximumPercent: 200
      MinimumHealthyPercent: 100
      DeploymentCircuitBreaker:
        Enable: true
        Rollback: true
    EnableECSManagedTags: true
    PropagateTags: SERVICE
    HealthCheckGracePeriodSeconds: 60
    DeploymentController:
      Type: ECS                # ECS, CODE_DEPLOY, or EXTERNAL
```

### Rolling Update Configuration

```yaml
DeploymentConfiguration:
  MaximumPercent: 200
  MinimumHealthyPercent: 100
  DeploymentCircuitBreaker:
    Enable: true
    Rollback: true
```

**Parameters:**
- `MaximumPercent`: Maximum percentage of tasks running during deployment (100-200)
- `MinimumHealthyPercent`: Minimum percentage of healthy tasks (0-100)
- `DeploymentCircuitBreaker`: Enable automatic rollback on failure

**Common configurations:**

**Fast rolling update:**
```yaml
DeploymentConfiguration:
  MaximumPercent: 200
  MinimumHealthyPercent: 50
```

**Conservative rolling update:**
```yaml
DeploymentConfiguration:
  MaximumPercent: 150
  MinimumHealthyPercent: 100
```

### Auto Scaling Configuration

```yaml
ScalableTarget:
  Type: AWS::ApplicationAutoScaling::ScalableTarget
  Properties:
    MaxCapacity: 10
    MinCapacity: 2
    ResourceId: !Sub 'service/${ECSCluster}/${ECSService}'
    RoleARN: !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-application-autoscaling-role'
    ScalableDimension: ecs:service:DesiredCount
    ServiceNamespace: ecs

TargetTrackingScalingPolicy:
  Type: AWS::ApplicationAutoScaling::ScalingPolicy
  Properties:
    PolicyName: CPUScalingPolicy
    PolicyType: TargetTrackingScaling
    ScalingTargetId: !Ref ScalableTarget
    TargetTrackingScalingPolicyConfiguration:
      TargetValue: 70.0
      PredefinedMetricSpecification:
        PredefinedMetricType: ECSServiceAverageCPUUtilization
      ScaleInCooldown: 300
      ScaleOutCooldown: 60
```

**Memory-based scaling:**

```yaml
TargetTrackingScalingPolicy:
  Type: AWS::ApplicationAutoScaling::ScalingPolicy
  Properties:
    PolicyName: MemoryScalingPolicy
    PolicyType: TargetTrackingScaling
    ScalingTargetId: !Ref ScalableTarget
    TargetTrackingScalingPolicyConfiguration:
      TargetValue: 80.0
      PredefinedMetricSpecification:
        PredefinedMetricType: ECSServiceAverageMemoryUtilization
```

**Custom metric scaling:**

```yaml
TargetTrackingScalingPolicy:
  Type: AWS::ApplicationAutoScaling::ScalingPolicy
  Properties:
    PolicyName: CustomScalingPolicy
    PolicyType: TargetTrackingScaling
    ScalingTargetId: !Ref ScalableTarget
    TargetTrackingScalingPolicyConfiguration:
      TargetValue: 1000.0
      CustomizedMetricSpecification:
        MetricName: RequestCountPerTarget
        Namespace: AWS/ApplicationELB
        Statistic: Sum
        Dimensions:
          - Name: TargetGroup
            Value: !Ref TargetGroup
```

### Scheduled Scaling

```yaml
ScheduledActionScaleOut:
  Type: AWS::ApplicationAutoScaling::ScheduledAction
  Properties:
    ScheduledActionName: ScaleOutBusinessHours
    Schedule: 'cron(0 8 ? * MON-FRI *)'
    ScalableDimension: ecs:service:DesiredCount
    ServiceNamespace: ecs
    ResourceId: !Sub 'service/${ECSCluster}/${ECSService}'
    ScalableTargetAction:
      MinCapacity: 4
      MaxCapacity: 10

ScheduledActionScaleIn:
  Type: AWS::ApplicationAutoScaling::ScheduledAction
  Properties:
    ScheduledActionName: ScaleInAfterHours
    Schedule: 'cron(0 18 ? * MON-FRI *)'
    ScalableDimension: ecs:service:DesiredCount
    ServiceNamespace: ecs
    ResourceId: !Sub 'service/${ECSCluster}/${ECSService}'
    ScalableTargetAction:
      MinCapacity: 2
      MaxCapacity: 4
```

### Task Placement Strategies

```yaml
ECSService:
  Type: AWS::ECS::Service
  Properties:
    PlacementStrategies:
      - Type: binpack
        Field: memory
      - Type: spread
        Field: attribute:ecs.availability-zone
    PlacementConstraints:
      - Type: distinctInstance
      - Type: memberOf
        Expression: attribute:ecs.instance-type =~ t2.*
```

**Available strategies:**
- `random`: Random placement
- `binpack`: Bin pack based on CPU or memory
- `spread`: Spread tasks across availability zones or instances

## CloudFormation Stack Updates

### Basic Stack Update

```yaml
DeployStack:
  Type: Custom::DeployStack
  Properties:
    ServiceToken: !GetAtt LambdaFunction.Arn
```

**GitHub Actions step:**

```yaml
- name: Update CloudFormation stack
  run: |
    aws cloudformation deploy \
      --template-file infrastructure/ecs-stack.yaml \
      --stack-name my-app-ecs \
      --capabilities CAPABILITY_NAMED_IAM \
      --parameter-overrides \
        Environment=production \
        ImageUrl=${{ steps.login-ecr.outputs.registry }}/my-app:${{ github.sha }} \
        DesiredCount=3 \
        CPU=512 \
        Memory=1024
```

### Change Set Deployment

```yaml
- name: Create change set
  run: |
    aws cloudformation create-change-set \
      --stack-name my-app-ecs \
      --change-set-name my-changeset-${{ github.sha }} \
      --template-body file://infrastructure/ecs-stack.yaml \
      --parameters \
        ParameterKey=Environment,ParameterValue=production \
        ParameterKey=ImageUrl,ParameterValue=${{ steps.login-ecr.outputs.registry }}/my-app:${{ github.sha }} \
      --capabilities CAPABILITY_NAMED_IAM \
      --change-set-type UPDATE

- name: Wait for change set creation
  run: |
    aws cloudformation wait change-set-create-complete \
      --stack-name my-app-ecs \
      --change-set-name my-changeset-${{ github.sha }}

- name: Describe change set
  run: |
    aws cloudformation describe-change-set \
      --stack-name my-app-ecs \
      --change-set-name my-changeset-${{ github.sha }}

- name: Execute change set
  run: |
    aws cloudformation execute-change-set \
      --stack-name my-app-ecs \
      --change-set-name my-changeset-${{ github.sha }}

- name: Wait for stack update
  run: |
    aws cloudformation wait stack-update-complete \
      --stack-name my-app-ecs
```

### Stack Policies

**Update policy to protect resources:**

```yaml
- name: Deploy with stack policy
  run: |
    aws cloudformation deploy \
      --template-file infrastructure/ecs-stack.yaml \
      --stack-name my-app-ecs \
      --stack-policy-url file://infrastructure/stack-policy.json \
      --capabilities CAPABILITY_NAMED_IAM \
      --parameter-overrides \
        ImageUrl=${{ steps.login-ecr.outputs.registry }}/my-app:${{ github.sha }}
```

**stack-policy.json:**

```json
{
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "Update:Modify",
      "Principal": "*",
      "Resource": "*"
    },
    {
      "Effect": "Deny",
      "Action": ["Update:Replace", "Update:Delete"],
      "Principal": "*",
      "Resource": ["LogicalResourceId/DatabaseInstance"]
    }
  ]
}
```

### Nested Stack Updates

```yaml
MainStack:
  Type: AWS::CloudFormation::Stack
  Properties:
    TemplateURL: !Sub 'https://${S3Bucket}.s3.amazonaws.com/templates/main.yaml'
    Parameters:
      Environment: !Ref Environment
      VpcId: !ImportValue NetworkStack-VPCId

ECSStack:
  Type: AWS::CloudFormation::Stack
  Properties:
    TemplateURL: !Sub 'https://${S3Bucket}.s3.amazonaws.com/templates/ecs.yaml'
    Parameters:
      ClusterName: !Ref ClusterName
      ImageUrl: !Ref ImageUrl
      DesiredCount: !Ref DesiredCount
```

**Update nested stack:**

```yaml
- name: Update nested stack
  run: |
    aws cloudformation update-stack \
      --stack-name my-app-main \
      --use-previous-template \
      --parameters \
        ParameterKey=ImageUrl,ParameterValue=${{ steps.login-ecr.outputs.registry }}/my-app:${{ github.sha }}
```

### Drift Detection

```yaml
- name: Detect drift
  run: |
    aws cloudformation detect-stack-drift \
      --stack-name my-app-ecs

- name: Wait for drift detection
  run: |
    aws cloudformation wait stack-drift-detection-complete \
      --stack-name my-app-ecs

- name: Describe drift
  run: |
    aws cloudformation describe-stack-drift-detection-status \
      --stack-name my-app-ecs \
      --drift-detection-id $(aws cloudformation describe-stack-resource-drifts \
        --stack-name my-app-ecs \
        --query 'Drifts[0].StackResourceDriftId' \
        --output text)
```

## Security Considerations

### IAM Best Practices

**Least privilege roles:**

```yaml
ECSDeployRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Principal:
            Federated: !Sub 'arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com'
          Action: sts:AssumeRoleWithWebIdentity
          Condition:
            StringEquals:
              token.actions.githubusercontent.com:aud: sts.amazonaws.com
            StringLike:
              token.actions.githubusercontent.com:sub: repo:my-org/my-repo:ref:refs/heads/main
    Policies:
      - PolicyName: MinimalECSDeployPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - ecs:DescribeServices
                - ecs:DescribeTaskDefinition
                - ecs:RegisterTaskDefinition
                - ecs:UpdateService
              Resource: !Sub 'arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:service/my-cluster/my-service'
            - Effect: Allow
              Action:
                - ecr:BatchCheckLayerAvailability
                - ecr:GetDownloadUrlForLayer
                - ecr:InitiateLayerUpload
                - ecr:UploadLayerPart
                - ecr:CompleteLayerUpload
                - ecr:PutImage
              Resource: !Sub 'arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/my-app'
```

**Role chaining for multi-account access:**

```yaml
CrossAccountRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Principal:
            AWS: !Sub 'arn:aws:iam::${SourceAccountId}:role/github-actions-ecs-role'
          Action: sts:AssumeRole
    Policies:
      - PolicyName: CrossAccountDeployPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - ecs:UpdateService
                - ecs:RegisterTaskDefinition
              Resource: !Sub 'arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*'
```

### Secrets Management

**Store sensitive data in Secrets Manager:**

```yaml
DatabaseSecret:
  Type: AWS::SecretsManager::Secret
  Properties:
    Name: !Sub '${Environment}/my-app/database'
    Description: Database credentials for my-app
    GenerateSecretString:
      SecretStringTemplate: '{"username": "myapp_user"}'
      GenerateStringKey: password
      PasswordLength: 32
      ExcludeCharacters: '"@/'

SecretRotationSchedule:
  Type: AWS::SecretsManager::RotationSchedule
  Properties:
    SecretId: !Ref DatabaseSecret
    RotationLambdaARN: !Ref RotationLambda
    RotationRules:
      AutomaticallyAfterDays: 30
```

**Reference secrets in task definition:**

```yaml
ContainerDefinitions:
  - Name: my-app
    Secrets:
      - Name: DB_HOST
        ValueFrom: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Environment}/my-app/database:host'
      - Name: DB_PASSWORD
        ValueFrom: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Environment}/my-app/database:password'
```

### Network Security

**VPC configuration for ECS:**

```yaml
ECSService:
  Type: AWS::ECS::Service
  Properties:
    NetworkConfiguration:
      AwsvpcConfiguration:
        Subnets:
          - !Ref PrivateSubnetA
          - !Ref PrivateSubnetB
        SecurityGroups:
          - !Ref ContainerSecurityGroup
        AssignPublicIp: DISABLED

ContainerSecurityGroup:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: Security group for ECS containers
    VpcId: !Ref VPC
    SecurityGroupIngress:
      - Description: Allow traffic from load balancer
        IpProtocol: tcp
        FromPort: 8080
        ToPort: 8080
        SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
    SecurityGroupEgress:
      - Description: Allow HTTPS outbound
        IpProtocol: tcp
        FromPort: 443
        ToPort: 443
        CidrIp: 0.0.0.0/0
```

**VPC endpoints for private communication:**

```yaml
ECREndpoint:
  Type: AWS::EC2::VPCEndpoint
  Properties:
    ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ecr.dkr'
    VpcId: !Ref VPC
    VpcEndpointType: Interface
    SubnetIds:
      - !Ref PrivateSubnetA
      - !Ref PrivateSubnetB
    SecurityGroupIds:
      - !Ref VPCEndpointSecurityGroup
    PrivateDnsEnabled: true

ECSEndpoint:
  Type: AWS::EC2::VPCEndpoint
  Properties:
    ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ecs'
    VpcId: !Ref VPC
    VpcEndpointType: Interface
    SubnetIds:
      - !Ref PrivateSubnetA
      - !Ref PrivateSubnetB
    SecurityGroupIds:
      - !Ref VPCEndpointSecurityGroup
    PrivateDnsEnabled: true

CloudWatchLogsEndpoint:
  Type: AWS::EC2::VPCEndpoint
  Properties:
    ServiceName: !Sub 'com.amazonaws.${AWS::Region}.logs'
    VpcId: !Ref VPC
    VpcEndpointType: Interface
    SubnetIds:
      - !Ref PrivateSubnetA
      - !Ref PrivateSubnetB
    SecurityGroupIds:
      - !Ref VPCEndpointSecurityGroup
    PrivateDnsEnabled: true
```

### Encryption

**KMS encryption for ECR:**

```yaml
EncryptionKey:
  Type: AWS::KMS::Key
  Properties:
    Description: KMS key for ECR encryption
    KeyPolicy:
      Version: '2012-10-17'
      Statement:
        - Sid: Enable IAM User Permissions
          Effect: Allow
          Principal:
            AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
          Action: 'kms:*'
          Resource: 

…(truncated)
