--- - name: Configure and Deploy ECS Application (Enterprise Security) hosts: inventory_hosts # DO NOT use blanket root access become: no gather_facts: yes vars: ecs_cluster_name: "nvhi-atsila-cluster" service_name: "nvhi-atsila-cluster-service" task_family: "nvhi-atsila-cluster-task" container_name: "nvhi-atsila-microservice" app_version: "{{ app_version | default('latest') }}" aws_region: "{{ aws_region | default('us-east-2') }}" log_group: "/ecs/{{ ecs_cluster_name }}" # Security: Use dedicated service account ecs_user: "ecs-user" ecs_group: "ecs-group" pre_tasks: - name: Validate required variables assert: that: - ecs_cluster_name is defined - aws_region is defined - aws_account_id is defined - task_execution_role_arn is defined fail_msg: "Required variables missing. Check app_version, aws_account_id, task_execution_role_arn" tags: [validation] - name: Test connectivity ping: tags: [validation] # Security: Create dedicated service account - name: Create ECS service group group: name: "{{ ecs_group }}" state: present become: yes become_user: root tags: [security, users] - name: Create ECS service user user: name: "{{ ecs_user }}" group: "{{ ecs_group }}" system: yes shell: /bin/bash home: /home/{{ ecs_user }} create_home: yes state: present become: yes become_user: root tags: [security, users] - name: Add ECS user to docker group user: name: "{{ ecs_user }}" groups: docker append: yes become: yes become_user: root tags: [security, users] tasks: # Infrastructure Setup - Only escalate when necessary - name: Update system packages yum: name: '*' state: latest update_cache: yes become: yes become_user: root async: 300 poll: 0 register: yum_update tags: [infrastructure] - name: Wait for package update to complete async_status: jid: "{{ yum_update.ansible_job_id }}" register: update_result until: update_result.finished retries: 30 delay: 10 tags: [infrastructure] - name: Install required packages yum: name: - docker - ecs-init - curl - wget - jq state: present become: yes become_user: root retries: 3 delay: 5 tags: [infrastructure] # Security: Configure Docker securely - name: Create Docker configuration directory file: path: /etc/docker state: directory mode: '0755' owner: root group: root become: yes become_user: root tags: [infrastructure, security] - name: Configure Docker daemon securely copy: dest: /etc/docker/daemon.json content: | { "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "live-restore": true, "userland-proxy": false, "no-new-privileges": true } mode: '0644' owner: root group: root become: yes become_user: root notify: restart docker tags: [infrastructure, security] - name: Start and enable Docker systemd: name: docker state: started enabled: true daemon_reload: true become: yes become_user: root register: docker_service tags: [infrastructure] - name: Verify Docker is running command: docker info register: docker_check failed_when: docker_check.rc != 0 retries: 3 delay: 5 changed_when: false # Security: Run as regular user (ECS user is in docker group) become: yes become_user: "{{ ecs_user }}" tags: [infrastructure, validation] # Security: Create ECS directory with proper permissions - name: Create ECS config directory file: path: /etc/ecs state: directory mode: '0755' owner: root group: "{{ ecs_group }}" become: yes become_user: root tags: [infrastructure, security] - name: Configure ECS agent copy: dest: /etc/ecs/ecs.config content: | ECS_CLUSTER={{ ecs_cluster_name }} ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","awslogs"] ECS_ENABLE_TASK_IAM_ROLE=true ECS_ENABLE_CONTAINER_METADATA=true ECS_CONTAINER_STOP_TIMEOUT=30s # Security: Disable privileged containers by default ECS_DISABLE_PRIVILEGED=true # Security: Enable AppArmor/SELinux support ECS_SELINUX_CAPABLE=true ECS_APPARMOR_CAPABLE=true mode: '0640' # Security: More restrictive permissions owner: root group: "{{ ecs_group }}" # Security: Group ownership for ECS backup: yes become: yes become_user: root notify: restart ecs tags: [infrastructure, security] # Security: Configure ECS agent service with proper user - name: Create ECS service override directory file: path: /etc/systemd/system/ecs.service.d state: directory mode: '0755' owner: root group: root become: yes become_user: root tags: [infrastructure, security] - name: Configure ECS service security settings copy: dest: /etc/systemd/system/ecs.service.d/security.conf content: | [Service] # Security: Additional hardening NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true # Allow access to ECS directories ReadWritePaths=/var/lib/ecs /var/log/ecs /etc/ecs mode: '0644' owner: root group: root become: yes become_user: root notify: - reload systemd - restart ecs tags: [infrastructure, security] - name: Start and enable ECS agent systemd: name: ecs state: started enabled: true daemon_reload: true become: yes become_user: root tags: [infrastructure] - name: Wait for ECS agent to register shell: | count=0 while [ $count -lt 30 ]; do instances=$(aws ecs list-container-instances --cluster {{ ecs_cluster_name }} --region {{ aws_region }} --query 'length(containerInstanceArns)' --output text 2>/dev/null || echo "0") if [ "$instances" != "0" ] && [ "$instances" != "None" ]; then echo "ECS agent registered successfully" exit 0 fi echo "Waiting for ECS agent registration (attempt $((count+1))/30)..." sleep 10 count=$((count+1)) done echo "ECS agent failed to register" exit 1 environment: AWS_DEFAULT_REGION: "{{ aws_region }}" delegate_to: localhost run_once: true # Security: Run AWS CLI as regular user with proper AWS credentials become: no tags: [infrastructure] # Application Deployment - No root required - name: Create CloudWatch log group shell: | aws logs create-log-group --log-group-name "{{ log_group }}" --region {{ aws_region }} 2>/dev/null || echo "Log group exists" aws logs put-retention-policy --log-group-name "{{ log_group }}" --retention-in-days 7 --region {{ aws_region }} 2>/dev/null || echo "Retention policy exists" environment: AWS_DEFAULT_REGION: "{{ aws_region }}" delegate_to: localhost run_once: true # Security: No root required for AWS API calls become: no tags: [deployment] # Security: Create temp file in user's home directory - name: Create task definition file copy: dest: "/tmp/task-definition-{{ ansible_date_time.epoch }}.json" content: | { "family": "{{ task_family }}", "executionRoleArn": "{{ task_execution_role_arn }}", "networkMode": "bridge", "requiresCompatibilities": ["EC2"], "cpu": "256", "memory": "512", "containerDefinitions": [ { "name": "{{ container_name }}", "image": "{{ aws_account_id }}.dkr.ecr.{{ aws_region }}.amazonaws.com/{{ container_name }}:{{ app_version }}", "cpu": 256, "memory": 512, "essential": true, "user": "1000:1000", "readonlyRootFilesystem": true, "portMappings": [ { "containerPort": 8080, "hostPort": 8080, "protocol": "tcp" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "{{ log_group }}", "awslogs-region": "{{ aws_region }}", "awslogs-stream-prefix": "ecs" } }, "healthCheck": { "command": [ "CMD-SHELL", "curl -f http://localhost:8080/health || exit 1" ], "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 60 }, "tmpfs": [ { "containerPath": "/tmp", "size": 100 } ], "mountPoints": [], "volumesFrom": [] } ] } mode: '0644' # Security: File owned by current user, not root owner: "{{ ansible_user | default(ansible_ssh_user) }}" group: "{{ ansible_user | default(ansible_ssh_user) }}" delegate_to: localhost run_once: true # Security: No root required become: no register: task_def_file tags: [deployment, security] - name: Register task definition shell: | aws ecs register-task-definition \ --cli-input-json file://{{ task_def_file.dest }} \ --region {{ aws_region }} \ --output json environment: AWS_DEFAULT_REGION: "{{ aws_region }}" delegate_to: localhost run_once: true # Security: No root required for AWS API calls become: no register: task_registration tags: [deployment] - name: Update ECS service shell: | aws ecs update-service \ --cluster {{ ecs_cluster_name }} \ --service {{ service_name }} \ --task-definition {{ task_family }} \ --desired-count 1 \ --force-new-deployment \ --region {{ aws_region }} \ --output json environment: AWS_DEFAULT_REGION: "{{ aws_region }}" delegate_to: localhost run_once: true # Security: No root required become: no register: service_update tags: [deployment] - name: Wait for service deployment to complete shell: | echo "Waiting for service to stabilize..." count=0 while [ $count -lt 30 ]; do service_status=$(aws ecs describe-services \ --cluster {{ ecs_cluster_name }} \ --services {{ service_name }} \ --region {{ aws_region }} \ --query 'services[0]' \ --output json 2>/dev/null) if [ $? -eq 0 ]; then running=$(echo "$service_status" | jq -r '.runningCount // 0') pending=$(echo "$service_status" | jq -r '.pendingCount // 0') echo "Running: $running, Pending: $pending" if [ "$running" -ge "1" ] && [ "$pending" -eq "0" ]; then echo "Service deployment completed successfully" exit 0 fi fi echo "Waiting for deployment completion (attempt $((count+1))/30)..." sleep 20 count=$((count+1)) done echo "Service deployment did not complete within expected time" exit 1 environment: AWS_DEFAULT_REGION: "{{ aws_region }}" delegate_to: localhost run_once: true # Security: No root required become: no tags: [deployment] # Health Verification - No root required - name: Wait for application health check uri: url: "http://{{ ansible_default_ipv4.address }}:8080/health" method: GET timeout: 10 status_code: 200 register: health_check until: health_check.status == 200 retries: 10 delay: 15 # Security: No root required for HTTP requests become: no tags: [verification] - name: Display deployment summary debug: msg: | ======================================== 🎉 SECURE DEPLOYMENT COMPLETED ======================================== Cluster: {{ ecs_cluster_name }} Service: {{ service_name }} Task Family: {{ task_family }} Image Version: {{ app_version }} Instance IP: {{ ansible_default_ipv4.address }} Health Status: HEALTHY Security: Non-root containers, least privilege Application URL: http://{{ ansible_default_ipv4.address }}:8080 ======================================== tags: [reporting] handlers: - name: reload systemd systemd: daemon_reload: yes become: yes become_user: root - name: restart docker systemd: name: docker state: restarted become: yes become_user: root - name: restart ecs systemd: name: ecs state: restarted daemon_reload: true become: yes become_user: root post_tasks: - name: Cleanup temporary files file: path: "{{ item }}" state: absent loop: - "/tmp/task-definition-{{ ansible_date_time.epoch }}.json" delegate_to: localhost # Security: No root required for cleanup become: no tags: [cleanup] # Security: Audit log - name: Log deployment action lineinfile: path: /var/log/ecs-deployments.log line: "{{ ansible_date_time.iso8601 }} - Deployment v{{ app_version }} by {{ ansible_user | default('unknown') }} from {{ ansible_env.SSH_CLIENT.split()[0] | default('unknown') }}" create: yes mode: '0644' owner: root group: "{{ ecs_group }}" become: yes become_user: root tags: [audit, security]