├── db └── password.txt ├── backend ├── requirements.txt ├── Dockerfile └── hello.py ├── frontend ├── Dockerfile └── conf ├── docker-compose.prod.rds.yaml ├── README.md ├── docker-compose.yaml ├── operations ├── buildspec.yaml ├── deployspec.yaml └── code-pipeline-cloudformation.yaml ├── .github └── workflows │ └── docker-hub.yaml ├── docker-compose.prod.migrate.yaml └── docker-compose.prod.scaling.yaml /db/password.txt: -------------------------------------------------------------------------------- 1 | db-78n9n -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.1.0 2 | mysql-connector==2.2.9 3 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.13-alpine 2 | COPY ./conf /etc/nginx/conf.d/default.conf -------------------------------------------------------------------------------- /frontend/conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 3000; 3 | server_name localhost; 4 | location / { 5 | proxy_pass http://backend:5000; 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | WORKDIR /code 3 | COPY ./ /code/ 4 | 5 | RUN ls -ltr /code 6 | RUN pip install -r requirements.txt 7 | ENV FLASK_APP hello.py 8 | CMD flask run --host=0.0.0.0 9 | -------------------------------------------------------------------------------- /docker-compose.prod.rds.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | environment: 4 | - MY_SQL_DATABASE=workshop 5 | - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password 6 | - MY_SQL_HOST=${RDS_ENDPOINT} 7 | - MY_SQL_USER=admin 8 | - DB_TYPE=rds 9 | 10 | secrets: 11 | db-password: 12 | name: ${RDS_DB_SECRET_ARN} 13 | external: true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Compose ECS Sample 2 | 3 | This repository is the codebase used during the docker build talk on 02/04/2021. Entire talk is available at https://www.youtube.com/watch?v=2W_AQhWTmRw 4 | For step by step instructions on how to use this sample application go through https://docker.awsworkshop.io/ 5 | 6 | ### Provide Feedback 7 | 8 | * Regarding this repository - https://github.com/anshrma/docker-compose-ecs-sample/issues 9 | 10 | * Regarding Docker Compose - https://github.com/docker/roadmap/issues 11 | 12 | ## Reach me 13 | 14 | * Links to LinkedIn, Twitter, Email at https://github.com/anshrma 15 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: mysql:8.0.19 4 | command: '--default-authentication-plugin=mysql_native_password' 5 | restart: always 6 | secrets: 7 | - db-password 8 | volumes: 9 | - db-data:/var/lib/mysql 10 | networks: 11 | - backnet 12 | environment: 13 | - MYSQL_DATABASE=example 14 | - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password 15 | backend: 16 | build: 17 | context: ./backend 18 | restart: always 19 | secrets: 20 | - db-password 21 | # ports: 22 | # - 5000:5000 23 | networks: 24 | - backnet 25 | - frontnet 26 | depends_on: 27 | - db 28 | environment: 29 | - DB_TYPE=docker 30 | frontend: 31 | build: 32 | context: ./frontend 33 | restart: always 34 | ports: 35 | - 3000:3000 36 | networks: 37 | - frontnet 38 | depends_on: 39 | - backend 40 | volumes: 41 | db-data: 42 | secrets: 43 | db-password: 44 | # This is mounted to /run/secrets/ onto the container using it 45 | file: db/password.txt 46 | networks: 47 | backnet: 48 | frontnet: 49 | -------------------------------------------------------------------------------- /operations/buildspec.yaml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | phases: 3 | install: 4 | runtime-versions: 5 | python: 3.8 6 | commands: 7 | - echo "Performing manual install of compose cli" 8 | - curl -L -o docker-linux-amd64.tar.gz https://github.com/docker/compose-cli/releases/download/v1.0.10/docker-linux-amd64.tar.gz 9 | - tar xzf docker-linux-amd64.tar.gz 10 | - chmod +x docker/docker 11 | - ls -ltr 12 | - docker/docker compose --help 13 | - which docker 14 | - ln -s $(which docker) /usr/local/bin/com.docker.cli 15 | pre_build: 16 | commands: 17 | - echo Logging in to Docker Hub... 18 | - docker login --username $DOCKERHUB_USERNAME --password $DOCKERHUB_PASSWORD 19 | 20 | build: 21 | commands: 22 | - echo Build started on `date` 23 | - docker/docker compose build 24 | - echo "Tagging Docker image for Docker Hub" 25 | - docker images 26 | - docker tag src_backend:latest ${DOCKERHUB_USERNAME}/docker-compose-ecs-sample_backend:${CODEBUILD_RESOLVED_SOURCE_VERSION} 27 | - docker tag src_frontend:latest ${DOCKERHUB_USERNAME}/docker-compose-ecs-sample_frontend:${CODEBUILD_RESOLVED_SOURCE_VERSION} 28 | - docker push ${DOCKERHUB_USERNAME}/docker-compose-ecs-sample_backend:${CODEBUILD_RESOLVED_SOURCE_VERSION} 29 | - docker push ${DOCKERHUB_USERNAME}/docker-compose-ecs-sample_frontend:${CODEBUILD_RESOLVED_SOURCE_VERSION} 30 | 31 | post_build: 32 | commands: 33 | - echo "build successful" -------------------------------------------------------------------------------- /operations/deployspec.yaml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | phases: 3 | install: 4 | runtime-versions: 5 | python: 3.8 6 | commands: 7 | - echo "Performing manual install of compose cli" 8 | - curl -L -o docker-linux-amd64.tar.gz https://github.com/docker/compose-cli/releases/download/v1.0.10/docker-linux-amd64.tar.gz 9 | - tar xzf docker-linux-amd64.tar.gz 10 | - chmod +x docker/docker 11 | - ls -ltr 12 | - docker/docker compose --help 13 | - which docker 14 | - ln -s $(which docker) /usr/local/bin/com.docker.cli 15 | pre_build: 16 | commands: 17 | - echo Logging in to Docker Hub... 18 | - docker login --username $DOCKERHUB_USERNAME --password $DOCKERHUB_PASSWORD 19 | - STS_RESPONSE=$(curl 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) 20 | - export AWS_ACCESS_KEY_ID=$(echo $STS_RESPONSE | jq .AccessKeyId | tr -d \") 21 | - export AWS_SECRET_ACCESS_KEY=$(echo $STS_RESPONSE | jq .SecretAccessKey | tr -d \") 22 | - export AWS_SESSION_TOKEN=$(echo $STS_RESPONSE | jq .Token | tr -d \") 23 | - echo "Create Docker ECS context" 24 | - docker/docker context create ecs ecs-workshop --from-env 25 | - aws sts get-caller-identity 26 | - echo "Change context to use ECS context" 27 | - docker/docker context use ecs-workshop 28 | 29 | build: 30 | commands: 31 | - DOCKER_HUB_ID=${DOCKERHUB_USERNAME} DOCKER_PULL_SECRETS_MANAGER=${DOCKER_PULL_SECRETS_MANAGER} docker/docker compose -f docker-compose.yaml -f docker-compose.prod.migrate.yaml -p ${PROJECT_NAME} up 32 | 33 | post_build: 34 | commands: 35 | - echo "deploy successful" -------------------------------------------------------------------------------- /.github/workflows/docker-hub.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Compose CI CD to DockerHub 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | docker-build-push-backend: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | name: Checkout 15 | uses: actions/checkout@v2 16 | - 17 | name: Set up QEMU 18 | uses: docker/setup-qemu-action@v1 19 | - 20 | name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v1 22 | - 23 | name: Login to DockerHub 24 | uses: docker/login-action@v1 25 | with: 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | - 29 | name: Build and push 30 | uses: docker/build-push-action@v2 31 | with: 32 | file: ./backend/Dockerfile 33 | push: true 34 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/backend:${{ github.sha }} 35 | docker-build-push-frontend: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - 39 | name: Checkout 40 | uses: actions/checkout@v2 41 | - 42 | name: Set up QEMU 43 | uses: docker/setup-qemu-action@v1 44 | - 45 | name: Set up Docker Buildx 46 | uses: docker/setup-buildx-action@v1 47 | - 48 | name: Login to DockerHub 49 | uses: docker/login-action@v1 50 | with: 51 | username: ${{ secrets.DOCKERHUB_USERNAME }} 52 | password: ${{ secrets.DOCKERHUB_TOKEN }} 53 | - 54 | name: Build and push 55 | uses: docker/build-push-action@v2 56 | with: 57 | file: ./frontend/Dockerfile 58 | push: true 59 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/frontend:${{ github.sha }} -------------------------------------------------------------------------------- /docker-compose.prod.migrate.yaml: -------------------------------------------------------------------------------- 1 | # x-aws-vpc: "vpc-FILL_ME" 2 | # x-aws-logs_retention: 30 3 | # x-aws-cloudformation: 4 | # Resources: 5 | # LoadBalancer: 6 | # Properties: 7 | # #Scheme: internal 8 | # Subnets: 9 | # - subnet-FILL_ME 10 | # - subnet-FILL_ME 11 | # # Certificates: 12 | # # - CertificateArn: "arn:aws:acm:certificate/123abc" 13 | # # Protocol: HTTPS 14 | # FrontendService: 15 | # Properties: 16 | # NetworkConfiguration: 17 | # AwsvpcConfiguration: 18 | # Subnets: 19 | # - subnet-FILL_ME 20 | # - subnet-FILL_ME 21 | 22 | services: 23 | frontend: 24 | image: ${DOCKER_HUB_ID}/docker-compose-ecs-sample_frontend:latest 25 | x-aws-pull_credentials: ${DOCKER_PULL_SECRETS_MANAGER} 26 | backend: 27 | image: ${DOCKER_HUB_ID}/docker-compose-ecs-sample_backend:latest 28 | x-aws-pull_credentials: ${DOCKER_PULL_SECRETS_MANAGER} 29 | logging: 30 | options: 31 | mode: non-blocking # Default is blocking 32 | max-buffer-size: 5m # Default is 1min 33 | # This enables task level IAM role for the service 34 | # x-aws-role: 35 | # Version: '2012-10-17' 36 | # Statement: 37 | # - Effect: Allow 38 | # Action: sqs:* 39 | # Resource: FILL_ME_ARN_OF_SQS 40 | # secrets: 41 | # db-password: 42 | # name: ${DOCKER_PULL_SECRETS_MANAGER} 43 | # external: true 44 | 45 | 46 | #Above overrides mysecrets 47 | # $ cat creds.json 48 | # { 49 | # "username":"Some secret name", 50 | # "password":"some secret credential" 51 | # } 52 | 53 | # Now create it using following 54 | # $ docker secret create pullcred /path/to/creds.json --context GIVE_ECS_CONTEXT_NAME 55 | # arn:aws:secretsmanager:eu-west-3:xxx:secret:pullcred -------------------------------------------------------------------------------- /docker-compose.prod.scaling.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | cap_add: 4 | - SYS_PTRACE 5 | deploy: 6 | replicas: 2 7 | # resources: 8 | # limits: 9 | # memory: 2Gb 10 | # cpus: '0.5' 11 | x-aws-autoscaling: 12 | min: 1 13 | max: 10 #required 14 | cpu: 75 15 | 16 | x-aws-cloudformation: 17 | Transform: AWS::SecretsManager-2020-07-23 18 | Outputs: 19 | MySQL: 20 | Description: RDSInstance Endpoint 21 | Value: 22 | Fn::GetAtt : 23 | - MySQL 24 | - Endpoint.Address 25 | Resources: 26 | DBSecurityGroup: 27 | Type: AWS::EC2::SecurityGroup 28 | Properties: 29 | SecurityGroupIngress: 30 | - IpProtocol: "-1" 31 | CidrIp : 172.31.0.0/16 32 | GroupDescription: "Ingress Rule for DBSecurity" 33 | RDSInstanceSecret: 34 | Type: AWS::SecretsManager::Secret 35 | Properties: 36 | Description: 'This is the secret for my RDS instance' 37 | Name: RDSInstanceSecret 38 | GenerateSecretString: 39 | SecretStringTemplate: '{"username": "admin"}' 40 | GenerateStringKey: 'password' 41 | PasswordLength: 16 42 | ExcludeCharacters: '"@/\' 43 | MySQL: 44 | Type: AWS::RDS::DBInstance 45 | DependsOn: RDSInstanceSecret 46 | Properties: 47 | VPCSecurityGroups: 48 | - Fn::GetAtt : 49 | - DBSecurityGroup 50 | - GroupId 51 | DBName: workshop 52 | DBInstanceClass: db.t2.micro 53 | AllocatedStorage: 20 54 | Engine: MySQL 55 | EngineVersion: 8.0.23 56 | MasterUsername: '{{resolve:secretsmanager:RDSInstanceSecret:SecretString:username}}' 57 | MasterUserPassword: '{{resolve:secretsmanager:RDSInstanceSecret:SecretString:password}}' 58 | -------------------------------------------------------------------------------- /backend/hello.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | import mysql.connector 4 | import uuid 5 | import os 6 | import json 7 | 8 | class DBManager: 9 | def __init__(self): 10 | db_type = os.environ.get('DB_TYPE') 11 | password_file = os.environ.get('MY_SQL_PASSWORD_FILE','/run/secrets/db-password') 12 | pf = open(password_file, 'r') 13 | password = None 14 | 15 | if db_type == 'rds': 16 | credentials = json.load(pf) 17 | password = credentials["password"] 18 | else: 19 | password = pf.read() 20 | 21 | database = os.environ.get('MY_SQL_DATABASE','example') 22 | host = os.environ.get('MY_SQL_HOST',"db") 23 | user = os.environ.get('MY_SQL_USER',"root") 24 | self.connection = mysql.connector.connect( 25 | user=user, 26 | password=password, 27 | host=host, # name of the mysql service as set in the docker-compose file 28 | database=database, 29 | auth_plugin='mysql_native_password' 30 | ) 31 | pf.close() 32 | self.cursor = self.connection.cursor() 33 | 34 | def populate_db(self): 35 | 36 | self.cursor.execute('DROP TABLE IF EXISTS blog') 37 | self.cursor.execute('CREATE TABLE blog (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, title VARCHAR(256))') 38 | self.cursor.execute("insert into blog values(NULL,'initial load')") 39 | 40 | self.connection.commit() 41 | 42 | def insert_records(self,id,name): 43 | data = (id,str(name)) 44 | query = ( 45 | "INSERT INTO blog(id,title)" 46 | "VALUES (%s, %s)" 47 | ) 48 | self.cursor.execute(query,data) 49 | self.connection.commit() 50 | 51 | def query_titles(self): 52 | self.cursor.execute('SELECT title FROM blog') 53 | rec = [] 54 | for c in self.cursor: 55 | rec.append(c[0]) 56 | return rec 57 | 58 | 59 | server = Flask(__name__) 60 | conn = None 61 | 62 | @server.route('/') 63 | def listName(): 64 | global conn 65 | if not conn: 66 | conn = DBManager() 67 | conn.populate_db() 68 | #conn.insert_records() 69 | rec = conn.query_titles() 70 | 71 | response = '' 72 | for c in rec: 73 | response = response + '