Basis for a secure AWS deployment pipeline

This pipeline provides a basis for a security-focused deployment process for Dockerized applications on AWS in Europe, with infrastructure as code, automated security scanning, and compliance with GDPR requirements.

Architecture overview

Developer → AWS CodeCommit → AWS CodeBuild (CI) → Amazon ECR (Container Registry) → Amazon ECS Fargate (Staging/Prod) → AWS WAF + CloudFront → Amazon CloudWatch + GuardDuty

All resources deployed in eu-west-1 (Ireland) with GDPR compliance.

Prerequisites

  • AWS account with admin permissions

  • AWS CLI v2 installed and configured

  • Docker installed locally

  • Git installed

  • Terraform v1.3+ (for infrastructure as code)

AWS infrastructure setup

Configure AWS organizations SCPs (If available)

# Example SCP to enforce EU region deployment
cat > eu-scp.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyNonEURegions",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [
            "eu-west-1",
            "eu-west-2",
            "eu-west-3",
            "eu-north-1",
            "eu-central-1",
            "eu-south-1"
          ]
        }
      }
    }
  ]
}
EOF

aws organizations create-policy \
  --name "EU-Deployment-Restriction" \
  --description "Restrict deployments to EU regions only" \
  --content file://eu-scp.json \
  --type SERVICE_CONTROL_POLICY

Set up base infrastructure with Terraform

Create main.tf for foundational infrastructure:

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.55" # Latest stable as of 2023
    }
  }
}

provider "aws" {
  region = "eu-west-1"
  default_tags {
    tags = {
      Environment     = "Shared"
      SecurityScheme  = "Tiered"
      DataClassification = "Confidential"
      Compliance     = "GDPR"
    }
  }
}

# Create separate VPC for each environment
module "vpc_prod" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 3.0"

  name = "prod-vpc"
  cidr = "10.10.0.0/16"
  azs  = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]

  private_subnets = ["10.10.1.0/24", "10.10.2.0/24", "10.10.3.0/24"]
  public_subnets  = ["10.10.101.0/24", "10.10.102.0/24", "10.10.103.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = false # High availability

  # Flow logs for security monitoring
  enable_flow_log                      = true
  create_flow_log_cloudwatch_log_group = true
  create_flow_log_cloudwatch_iam_role  = true
  flow_log_max_aggregation_interval    = 60
}

module "vpc_staging" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 3.0"

  name = "staging-vpc"
  cidr = "10.20.0.0/16"
  azs  = ["eu-west-1a", "eu-west-1b"]

  private_subnets = ["10.20.1.0/24", "10.20.2.0/24"]
  public_subnets  = ["10.20.101.0/24", "10.20.102.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true # Cost optimization for staging

  enable_flow_log = true
}

# ECR Repositories with image scanning
resource "aws_ecr_repository" "app" {
  name                 = "secure-app"
  image_tag_mutability = "IMMUTABLE" # Prevent tag overwrites

  image_scanning_configuration {
    scan_on_push = true # Automatic vulnerability scanning
  }

  encryption_configuration {
    encryption_type = "KMS" # Use AWS KMS for encryption
  }
}

# ECS Cluster with Fargate
resource "aws_ecs_cluster" "prod" {
  name = "prod-cluster"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }

  configuration {
    execute_command_configuration {
      logging = "OVERRIDE"
      log_configuration {
        cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_command.name
      }
    }
  }
}

resource "aws_cloudwatch_log_group" "ecs_command" {
  name              = "/aws/ecs/exec"
  retention_in_days = 365
  kms_key_id        = aws_kms_key.logs.arn
}

CI/CD pipeline configuration

AWS CodePipeline Setup

Create pipeline.tf:

# CodeBuild IAM Role with least privilege
resource "aws_iam_role" "codebuild" {
  name = "codebuild-secure-app-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "codebuild.amazonaws.com"
        }
      }
    ]
  })

  # Permissions boundary for additional safety
  permissions_boundary = aws_iam_policy.codebuild_boundary.arn
}

resource "aws_iam_policy" "codebuild_boundary" {
  name        = "CodeBuildPermissionsBoundary"
  description = "Restrict CodeBuild to minimal required permissions"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Deny"
        Action   = "*"
        Resource = "*"
        Condition = {
          StringNotEquals = {
            "aws:RequestedRegion" = "eu-west-1"
          }
        }
      },
      {
        Effect   = "Deny"
        Action   = [
          "iam:*",
          "organizations:*",
          "account:*"
        ]
        Resource = "*"
      }
    ]
  })
}

# CodeBuild project for CI
resource "aws_codebuild_project" "secure_app_ci" {
  name          = "secure-app-ci"
  description   = "CI pipeline for secure app with security scanning"
  service_role  = aws_iam_role.codebuild.arn
  build_timeout = 10

  artifacts {
    type = "NO_ARTIFACTS"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_MEDIUM"
    image                       = "aws/codebuild/standard:6.0" # Latest as of 2023
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
    privileged_mode             = true # Required for Docker

    environment_variable {
      name  = "ECR_REPO"
      value = aws_ecr_repository.app.repository_url
    }

    environment_variable {
      name  = "AWS_ACCOUNT_ID"
      value = data.aws_caller_identity.current.account_id
    }
  }

  source {
    type            = "CODECOMMIT"
    location        = aws_codecommit_repository.secure_app.clone_url_http
    git_clone_depth = 1
    buildspec       = <<EOF
version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin ${aws_ecr_repository.app.repository_url}
      - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image --download-db-only
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $ECR_REPO:latest .
      - docker tag $ECR_REPO:latest $ECR_REPO:$CODEBUILD_RESOLVED_SOURCE_VERSION
      - echo Running vulnerability scan...
      - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image --exit-code 1 --severity CRITICAL $ECR_REPO:latest
      - echo Scanning for secrets in code...
      - docker run --rm -v $PWD:/src ghcr.io/gitleaks/gitleaks:latest detect --source="/src" --verbose --redact
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...
      - docker push $ECR_REPO:latest
      - docker push $ECR_REPO:$CODEBUILD_RESOLVED_SOURCE_VERSION
      - echo Writing image definitions file...
      - printf '[{"name":"secure-app","imageUri":"%s"}]' $ECR_REPO:$CODEBUILD_RESOLVED_SOURCE_VERSION > imagedefinitions.json
artifacts:
  files: imagedefinitions.json
EOF
  }

  logs_config {
    cloudwatch_logs {
      group_name  = "/aws/codebuild/secure-app-ci"
      stream_name = "build-log"
    }
  }
}

# Pipeline for staging deployment
resource "aws_codepipeline" "staging" {
  name     = "secure-app-staging"
  role_arn = aws_iam_role.codepipeline.arn

  artifact_store {
    location = aws_s3_bucket.artifacts.bucket
    type     = "S3"
  }

  stage {
    name = "Source"
    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeCommit"
      version          = "1"
      output_artifacts = ["source_output"]

      configuration = {
        RepositoryName = aws_codecommit_repository.secure_app.repository_name
        BranchName     = "staging"
      }
    }
  }

  stage {
    name = "Build"
    action {
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["source_output"]
      output_artifacts = ["build_output"]
      version          = "1"

      configuration = {
        ProjectName = aws_codebuild_project.secure_app_ci.name
      }
    }
  }

  stage {
    name = "DeployToStaging"
    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "ECS"
      input_artifacts = ["build_output"]
      version         = "1"

      configuration = {
        ClusterName = aws_ecs_cluster.staging.name
        ServiceName = aws_ecs_service.staging.name
        FileName    = "imagedefinitions.json"
      }
    }
  }
}

Security hardening

Network security

# Web Application Firewall (WAF)
resource "aws_wafv2_web_acl" "app" {
  name        = "secure-app-acl"
  scope       = "REGIONAL"
  description = "WAF for secure app with OWASP Top 10 rules"

  default_action {
    allow {}
  }

  rule {
    name     = "AWS-AWSManagedRulesCommonRuleSet"
    priority = 1

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWSManagedRulesCommonRuleSet"
      sampled_requests_enabled   = true
    }
  }

  rule {
    name     = "RateLimit"
    priority = 2

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 1000
        aggregate_key_type = "IP"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "RateLimit"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "secure-app-waf"
    sampled_requests_enabled   = true
  }
}

# Associate WAF with ALB
resource "aws_wafv2_web_acl_association" "app" {
  resource_arn = aws_lb.app.arn
  web_acl_arn  = aws_wafv2_web_acl.app.arn
}

# Security Groups
resource "aws_security_group" "app" {
  name        = "secure-app-sg"
  description = "Allow HTTPS inbound and restrict outbound"
  vpc_id      = module.vpc_prod.vpc_id

  ingress {
    description      = "HTTPS from ALB"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    security_groups  = [aws_security_group.alb.id]
  }

  egress {
    description      = "Outbound to ECR and CloudWatch"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    prefix_list_ids  = [
      data.aws_ec2_managed_prefix_list.s3.id,
      data.aws_ec2_managed_prefix_list.ecr.id
    ]
  }

  tags = {
    Name = "secure-app-sg"
  }
}

IAM and secrets management

# KMS for encryption
resource "aws_kms_key" "secrets" {
  description             = "KMS key for app secrets"
  enable_key_rotation    = true
  deletion_window_in_days = 30
  policy = data.aws_iam_policy_document.kms_policy.json
}

# Secrets Manager for credentials
resource "aws_secretsmanager_secret" "db_credentials" {
  name        = "prod/db/credentials"
  description = "Database credentials for production"
  kms_key_id = aws_kms_key.secrets.arn

  recovery_window_in_days = 7 # Minimum for immediate deletion

  replica {
    region = "eu-west-2" # Secondary EU region for DR
  }
}

resource "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id     = aws_secretsmanager_secret.db_credentials.id
  secret_string = jsonencode({
    username = "app_user"
    password = "initial-password" # Rotate immediately after creation
  })
}

# IAM Policy for ECS Task
data "aws_iam_policy_document" "ecs_task" {
  statement {
    effect = "Allow"
    actions = [
      "secretsmanager:GetSecretValue",
      "kms:Decrypt"
    ]
    resources = [
      aws_secretsmanager_secret.db_credentials.arn,
      aws_kms_key.secrets.arn
    ]
  }

  statement {
    effect = "Allow"
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    resources = ["*"]
  }
}

Monitoring and maintenance

CloudWatch alarms and dashboards

resource "aws_cloudwatch_metric_alarm" "high_cpu" {
  alarm_name          = "secure-app-high-cpu"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors ECS CPU utilization"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    ClusterName = aws_ecs_cluster.prod.name
    ServiceName = aws_ecs_service.prod.name
  }
}

resource "aws_cloudwatch_dashboard" "main" {
  dashboard_name = "secure-app-dashboard"

  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6

        properties = {
          metrics = [
            ["AWS/ECS", "CPUUtilization", "ClusterName", aws_ecs_cluster.prod.name, "ServiceName", aws_ecs_service.prod.name],
            [".", "MemoryUtilization", ".", ".", ".", "."]
          ]
          period = 300
          stat   = "Average"
          region = "eu-west-1"
          title  = "ECS Service Metrics"
        }
      }
    ]
  })
}

AWS GuardDuty and Inspector

resource "aws_guardduty_detector" "primary" {
  enable = true

  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = true
      }
    }
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          enable = true
        }
      }
    }
  }
}

resource "aws_inspector2_enabler" "example" {
  account_ids    = [data.aws_caller_identity.current.account_id]
  resource_types = ["ECR", "EC2"]
}

Code example

For ECS task definition with security best practices, create a task-definition.json:

{
  "family": "secure-app",
  "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/secure-app-task-role",
  "networkMode": "awsvpc",
  "cpu": "1024",
  "memory": "2048",
  "requiresCompatibilities": ["FARGATE"],
  "runtimePlatform": {
    "cpuArchitecture": "X86_64",
    "operatingSystemFamily": "LINUX"
  },
  "containerDefinitions": [
    {
      "name": "secure-app",
      "image": "ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/secure-app:latest",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 8080,
          "hostPort": 8080,
          "protocol": "tcp"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/secure-app",
          "awslogs-region": "eu-west-1",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:eu-west-1:ACCOUNT_ID:secret:prod/db/credentials:password::"
        }
      ],
      "linuxParameters": {
        "initProcessEnabled": true,
        "sharedMemorySize": 256,
        "capabilities": {
          "drop": ["ALL"]
        },
        "maxSwap": 512,
        "swappiness": 60
      },
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 60
      }
    }
  ]
}

CI/CD pipeline security checklist

  • All infrastructure deployed in EU region (eu-west-1)

  • Immutable container tags in ECR

  • Automated vulnerability scanning in CI pipeline

  • Secret scanning in code (Gitleaks)

  • Least privilege IAM roles for CodeBuild/CodePipeline

  • WAF with OWASP rules enabled

  • Encrypted secrets with KMS and rotation

  • Network isolation with VPC and security groups

  • Container runtime security (no root, dropped capabilities)

  • Comprehensive monitoring with CloudWatch and GuardDuty


Last update: 2025-05-12 14:39