LoginSignup
3
3

More than 1 year has passed since last update.

【AWS】Django環境をDockerで構築してみた

Last updated at Posted at 2022-02-04

2022/2/5 環境変数ファイルをユーザーデータ内で作成するように変更

はじめに

以前EC2+ELB+RDSでDjango環境を構築してみましたが、今回はDockerコンテナでDjango環境を構築してみたいと思います。

今回はコンテナを使用して構築することが主目的となるため、ELBやマルチAZを使用したスケーラブルな構成は考慮しません。

以下の記事を参考にさせていただきました。

構成図

以下のような構成を目指します。
diagram01.png

EC2インスタンス OS:Amazon Linux 2
Webサーバ(リバースプロキシ):Nginx
APサーバ(WSGIサーバ):Gunicorn
データベース(RDS):MySQL

  • EC2インスタンスにDocker, Docker Composeをインストール
  • EC2インスタンス上でNginx, Gunicornをそれぞれ別々のコンテナとして稼働
  • 静的ファイルの保存先として一つのボリュームを各コンテナにマウント
    • ボリュームはEC2インスタンスのファイルシステムに保存される
  • 各種設定ファイルを保存するためのS3バケットを作成

※リソースはすべて東京リージョンに作成します。

作業環境

macOS Monterey バージョン 12.1

必要ファイルの準備

環境構築に使用する各ファイルの準備を行っていきます。

Django アプリケーション

Djangoアプリケーションについては基本的に公式チュートリアルに沿って作成をしました。

データベースへの接続情報などをDjangoプロジェクト内のsettings.pyに記載していますが、別ファイルとしてsettings_secret.pyを作成しそちらに情報を転記します。

settings_secret.py
import os

SECRET_KEY = '<プロジェクト作成時に生成されるキー>'

DB_NAME = os.environ.get('DB_NAME')
DB_USER = os.environ.get('DB_USER')
DB_PASS = os.environ.get('DB_PASS')
DB_HOST = os.environ.get('DB_HOST')
DB_PORT = os.environ.get('DB_PORT')
  • settings_secret.pyで定義した変数はsetting.pyでインポートする
  • Djangoプロジェクト作成時に生成されるキーを転記
  • データベース接続時に使用するパラメータは環境変数から取得するように指定
    • Gunicornコンテナをビルドする際に環境変数の値を指定

また、データベースへの接続設定や静的ファイルのパスなどの設定を変更する必要がありますので、settings.pyの修正を行います。
※修正部分のみ抜粋して記載します。

settings.py
from .settings_secret import *

DEBUG = False

ALLOWED_HOSTS = ["*"]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': DB_NAME,
        'USER': DB_USER,
        'PASSWORD': DB_PASS,
        'HOST': DB_HOST,
        'PORT': DB_PORT,
    }
}

STATIC_ROOT = '/public/static'
  • settings_secret.pyで定義した情報をインポート
  • デバッグモードを無効化
  • すべてのホストに対するアクセスのみ許可
  • 使用するデータベースとしてMySQLを指定
    • 接続に必要なパラメータはsettings_secret.pyで定義(環境変数を使用)
  • 静的ファイルのパスを/public/staticに指定
    • Nginxコンテナ, Gunicornコンテナの両方からアクセス可能な外部ボリューム

Nginx設定ファイル

NginxとGunicornを連携させるためにNginxの設定を編集します。
デフォルトでは/etc/nginx/conf.dに設定ファイル(.conf)を配置すると、設定として読み込まれます。
/etc/nginx/nginx.confhttpディレクティブ内のinclude /etc/nginx/conf.d/*.confによって読み込まれます。

mysite.conf
upstream django_app {
  server gunicorn:8000;
}

server {
  listen 80;

  location /static {
    alias /public/static;
  }

  location / {
    proxy_pass http://django_app;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
  • リクエストをポート80で受け付ける
  • upstreamとしてdjango_appを設定
    • 基本的にはdjango_appにリクエストを転送
    • gunicornに対して、ポート8000でアクセス
  • /staticのエイリアスとして/public/staticを指定
    • 静的ファイルについては/public/staticを参照

Dockerfile

Gunicorn、Nginxのコンテナを作成するにあたって次のDockerfileを使用します。

Gunicorn

Dockerfile(Gunicorn)
# gunicorn container
FROM centos:7
LABEL application="Django-Docker"

# Install os dependencies
RUN yum update -y && yum clean all
RUN yum install -y python3 git python3-devel mysql mysql-devel gcc

# Create app user
RUN groupadd -g 1000 app && useradd -u 1000 -g app app

# Copy files
COPY --chown=app:app /mysite /app
COPY --chown=app:app settings_secret.py /app/mysite/settings_secret.py

# Install application dependencies
RUN pip3 install -r /app/requirements.txt --no-cache-dir

# Create public volume
RUN mkdir /public
RUN chown app:app /public
VOLUME /public

# Working directory and application user
WORKDIR /app
USER app
  • CentOS7のイメージを使用
  • 必要なパッケージをインストール
  • アプリケーション実行用にユーザーapp, グループappを作成
    • グループappにユーザーappを追加
  • Dockerホスト内のDjangoアプリケーションmysite/appとしてコンテナにコピー
    • 所有者をapp:appに設定
  • Dockerホスト内のsettings_secret.pyをコンテナの/app/mysiteにコピー
    • 所有者をapp:appに設定
  • /app/requirements.txtに記載のPythonモジュールをインストール
  • 静的ファイル保存用ボリュームのマウント先をあらかじめ作成
    • 所有者をapp:appに設定
  • ワーキングディレクトリを/app、実行ユーザーをappにそれぞれ設定

Nginxコンテナ

Dockerfile(Nginx)
# nginx container
FROM nginx
LABEL application="Django-Docker"

# Copy conf file
RUN rm /etc/nginx/conf.d/default.conf
COPY mysite.conf /etc/nginx/conf.d/mysite.conf
  • Nginxのイメージを使用
  • Dockerホスト内のNginx設定ファイルmysite.confをコンテナの/etc/nginx/conf.dにコピー

docker-compose.yml

docker-compose.ymlの設定内容は以下となります。

docker-compose.yml
version: '2.4'

volumes:
  static_volume:
    driver: local

services:
  gunicorn:
    env_file: ./django/variables.env
    build:
      context: ./django
      dockerfile: Dockerfile
    command: gunicorn --workers 3 --bind=0.0.0.0:8000 mysite.wsgi
    volumes:
      - static_volume:/public
    ports:
      - 8000:8000
  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    volumes:
      - static_volume:/public
    ports:
      - 80:80
    depends_on:
      - gunicorn
  • 静的ファイル保存用にボリュームを作成
  • APサーバとしてgunicornサービスを定義
    • variables.envに記載の環境変数をコンテナで読み込む
    • ./django/DockerfileをDockerfileとして指定
    • Gunicornを実行するようにコマンドを指定
    • static_volumeをコンテナで/publicとしてマウント
    • ホスト側、コンテナ側ともにポートを8000で設定
  • Webサーバとしてnginxサービスを定義
    • ./nginx/DockerfileをDockerfileとして指定
    • static_volumeをコンテナで/publicとしてマウント
    • ホスト側、コンテナ側ともにポートを80で設定
    • サービスgunicornが先に起動するように依存関係を設定

CloudFormationテンプレート

今回の構成で使用するAWSリソースについてはCloudFormationを使用して作成します。
以下のテンプレートを使用して構成します。
※ユーザーデータ内のGitHubリポジトリのURL、S3バケットの名前は置き換えて使用してください。

Docker-Django.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Django environment on Docker
Parameters:
  VpcName:
    Type: String
    Description: VPC name
    Default: "Docker-VPC"
  VpcCidrBlock:
    Type: String
    Description: VPC CIDR block
    Default: "10.0.0.0/16"
  IgwName:
    Type: String
    Description: Internet gateway name
    Default: "Docker-IGW"
  PublicSubnetName:
    Type: String
    Description: Public subnet name
    Default: "Docker-Public-Subnet"
  PublicSubnet1aCidrBlock:
    Type: String
    Description: Public subnet CIDR block
    Default: "10.0.0.0/24"
  PublicRouteTableName:
    Type: String
    Description: Public route table name
    Default: "Docker-public-rt"
  PrivateSubnetName:
    Type: String
    Description: Private subnet name
    Default: "Docker-Private-Subnet"
  PrivateSubnet1aCidrBlock:
    Type: String
    Description: Private subnet 1a CIDR block
    Default: "10.0.2.0/24"
  PrivateSubnet1cCidrBlock:
    Type: String
    Description: Private subnet 1c CIDR block
    Default: "10.0.3.0/24"
  PrivateRouteTableName:
    Type: String
    Description: Private route table name
    Default: "Docker-private-rt"
  EC2SecurityGroupName:
    Type: String
    Description: EC2 security group name
    Default: "Docker-EC2-SG"
  RDSSecurityGroupName:
    Type: String
    Description: RDS security group name
    Default: "Docker-RDS-SG"
  HTTPAccessIp:
    Type: String
    Description: Source IP address of HTTP access
    Default: "0.0.0.0/0"
  SSHAccessIp:
    Type: String
    Description: Source IP address of SSH access
    Default: "0.0.0.0/0"
  InstanceName:
    Type: String
    Description: EC2 instance name
    Default: "Docker-EC2"
  KeyName:
    Type: String
    Description: Key pair name
    Default: "docker-keypair"
  ImageId:
    Type: String
    Description: Image ID of EC2 instance
    Default: "ami-0404778e217f54308"
  IAMRoleName:
    Type: String
    Description: IAM role name
    Default: "S3AccessRole"
  RDSSubnetGroupName:
    Type: String
    Description: RDS subnet group name
    Default: "docker-subnetgroup"
  DBInstanceIdentifier:
    Type: String
    Description: RDS instance identifier
    Default: "database-docker"
  DBInstanceClass:
    Type: String
    Description: RDS instance class
    Default: "db.t2.micro"
  DBStorageType:
    Type: String
    Description: RDS storage type
    Default: "gp2"
  DBStorageSize:
    Type: String
    Description: RDS storage size
    Default: "20"
  DBMaxStorageSize:
    Type: String
    Description: RDS max storage size
    Default: "1000"
  DBMultiAZ:
    Type: String
    Description: RDS multi AZ
    Default: 'False'
  DBPublicAccess:
    Type: String
    Description: RDS publicly accessible
    Default: "False"
  DBNameTag:
    Type: String
    Description: DB name tag
    Default: 'db-docker'

Resources:
  DockerVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidrBlock
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Ref VpcName
  DockerIGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref IgwName
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref DockerVPC
      InternetGatewayId: !Ref DockerIGW
  PublicSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Sub ${AWS::Region}a
      VpcId: !Ref DockerVPC
      CidrBlock: !Ref PublicSubnet1aCidrBlock
      Tags:
        - Key: Name
          Value: !Sub ${PublicSubnetName}-1a
  publicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref DockerVPC
      Tags:
        - Key: Name
          Value: !Ref PublicRouteTableName
  publicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref publicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref DockerIGW
  publicRouteTable1aAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1a
      RouteTableId: !Ref publicRouteTable
  PrivateSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Sub ${AWS::Region}a
      VpcId: !Ref DockerVPC
      CidrBlock: !Ref PrivateSubnet1aCidrBlock
      Tags:
        - Key: Name
          Value: !Sub ${PrivateSubnetName}-1a
  PrivateSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Sub ${AWS::Region}c
      VpcId: !Ref DockerVPC
      CidrBlock: !Ref PrivateSubnet1cCidrBlock
      Tags:
        - Key: Name
          Value: !Sub ${PrivateSubnetName}-1c
  privateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref DockerVPC
      Tags:
        - Key: Name
          Value: !Ref PrivateRouteTableName
  privateRouteTable1aAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1a
      RouteTableId: !Ref privateRouteTable
  privateRouteTable1cAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1c
      RouteTableId: !Ref privateRouteTable
  DockerEC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Ref EC2SecurityGroupName
      GroupDescription: !Ref EC2SecurityGroupName
      VpcId: !Ref DockerVPC
      SecurityGroupIngress:
        -
          IpProtocol: "tcp"
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref SSHAccessIp
        -
          IpProtocol: "tcp"
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref HTTPAccessIp
      Tags:
        - Key: Name
          Value: !Ref EC2SecurityGroupName
  DockerRDSSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Ref RDSSecurityGroupName
      GroupDescription: !Ref RDSSecurityGroupName
      VpcId: !Ref DockerVPC
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !Ref DockerEC2SG
      Tags:
        - Key: Name
          Value: !Ref RDSSecurityGroupName
  DockerEC2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: !Ref KeyName
      ImageId: !Ref ImageId
      InstanceType: "t2.micro"
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: "0"
          SubnetId: !Ref PublicSubnet1a
          GroupSet:
            - !Ref DockerEC2SG
      IamInstanceProfile: !Ref S3AccessInstanceProfile
      UserData:
        Fn::Base64: |
          #!/bin/bash
          #必要なパッケージをインストール
          yum update -y
          yum install -y git
          amazon-linux-extras install -y docker

          #Docker, Docker Composeのインストール・設定
          usermod -a -G docker ec2-user
          systemctl start docker.service
          systemctl enable docker.service
          curl -L https://github.com/docker/compose/releases/download/1.21.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
          chmod +x /usr/local/bin/docker-compose
          ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

          #Djangoアプリケーション等の必要ファイルをダウンロード
          mkdir /home/ec2-user/docker-django
          aws s3 cp s3://<S3バケット名>/ /home/ec2-user/docker-django --recursive
          mv /home/ec2-user/docker-django/.netrc /root/.netrc
          mkdir /root/.aws
          mv /home/ec2-user/docker-django/config /root/.aws/config
          mv /home/ec2-user/docker-django/credentials /root/.aws/credentials
          git clone <GithubリポジトリのURL> /home/ec2-user/docker-django/django/mysite

          #環境変数ファイルを作成
          touch /home/ec2-user/docker-django/django/variables.env
          DBName=`aws ssm get-parameters --name "FirstDBName" --query "Parameters[].Value" --output text`
          DBUser=`aws ssm get-parameters --name "db_user" --query "Parameters[].Value" --output text`
          DBPass=`aws ssm get-parameters --name "db_password" --with-decryption --query "Parameters[].Value" --output text`
          DBHost=`aws rds describe-db-instances --query "DBInstances[?DBName==\\\`$DBName\\\`].Endpoint.Address" --output text`
          DBPort=`aws rds describe-db-instances --query "DBInstances[?DBName==\\\`$DBName\\\`].Endpoint.Port" --output text`
          echo "DB_NAME="$DBName >> /home/ec2-user/docker-django/django/variables.env
          echo "DB_USER="$DBUser >> /home/ec2-user/docker-django/django/variables.env
          echo "DB_PASS="$DBPass >> /home/ec2-user/docker-django/django/variables.env
          echo "DB_HOST="$DBHost >> /home/ec2-user/docker-django/django/variables.env
          echo "DB_PORT="$DBPort >> /home/ec2-user/docker-django/django/variables.env

          #docker-djangoディレクトリの所有者をec2-userに変更
          chown -R ec2-user:ec2-user /home/ec2-user/docker-django
      Tags:
        - Key: Name
          Value: !Ref InstanceName
  S3AccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service: 'ec2.amazonaws.com'
            Action: 'sts:AssumeRole'
      Description: IAM role for EC2 access to S3
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AmazonS3FullAccess'
      RoleName: !Ref IAMRoleName
      Tags:
        - Key: Name
          Value: !Ref IAMRoleName
  S3AccessInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Ref IAMRoleName
      Roles:
        - !Ref S3AccessRole
  RDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupName: !Ref RDSSubnetGroupName
      DBSubnetGroupDescription: !Ref RDSSubnetGroupName
      SubnetIds:
        - !Ref PrivateSubnet1a
        - !Ref PrivateSubnet1c
      Tags:
        - Key: Name
          Value: !Ref RDSSubnetGroupName
  MySQL:
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: mysql
      EngineVersion: '8.0.23'
      DBInstanceIdentifier: !Ref DBInstanceIdentifier
      MasterUsername: '{{resolve:ssm:db_user:1}}'
      MasterUserPassword: '{{resolve:ssm-secure:db_password:1}}'
      DBInstanceClass: !Ref DBInstanceClass
      StorageType: !Ref DBStorageType
      AllocatedStorage: !Ref DBStorageSize
      MaxAllocatedStorage: !Ref DBMaxStorageSize
      MultiAZ: !Ref DBMultiAZ
      DBSubnetGroupName: !Ref RDSSubnetGroup
      PubliclyAccessible: !Ref DBPublicAccess
      VPCSecurityGroups:
        - !Ref DockerRDSSG
      Port: '3306'
      DBName: '{{resolve:ssm:FirstDBName:1}}'
      Tags:
        - Key: Name
          Value: DBNameTag
  • ネットワーク
    • パブリックサブネットが1つ, プライベートサブネットが2つの構成
    • RDSサブネットグループ作成のため、プライベートサブネットはAZを分けて作成
  • EC2インスタンス
    • S3にアクセスするためのIAMロール/インスタンスプロファイルを作成して適用
    • ユーザーデータで以下の処理を実行
      • Docker, Docker Composeをインストール
      • その他必要ファイルについてもユーザーデータ処理でコピー
      • Gunicornコンテナで設定する環境変数を定義したファイルを作成 ※詳細は下記に記載
  • RDS
    • 作成するデータベース名、マスターユーザー名、マスターユーザーのパスワードはSystems Manager パラメータストアから値を取得
    • 作成したRDSインスタンスのエンドポイント名をGunicornコンテナビルド時に環境変数として指定

※variables.envの内容は以下のようになります。

variables.env
DB_NAME=<データベースの名前>
DB_USER=<データベースのマスターユーザー名>
DB_PASS=<データベースのマスターユーザーのパスワード>
DB_HOST=<データベースのエンドポイント名>
DB_PORT=<データベースで使用するポート>

構築事前準備

構築作業を開始する前に準備を行います。

データベース パラメータ登録

Systems Manager パラメータストアにデータベース接続に使用するパラメータを事前に登録しておきます。
作成するデータベース名、マスターユーザー名、マスターユーザーのパスワードを設定します。
今回は以下のように設定します。

名前 種類
FirstDBName String mysite
db_user String admin
db_password SecureString password

DjangoアプリケーションをGithubにアップロード

DjangoアプリケーションをGithubにアップロードします。
また、EC2インスタンス起動時のユーザーデータ処理の中でGitHubリポジトリからDjangoアプリケーションをダウンロードするにあたって、処理中に対話型でユーザー・パスワードを求められないようにします。
実行ユーザーのホームディレクトリに「.netrc」というファイルを作成することで、ファイルに記述した認証情報を使用することができます。

.netrc
machine github.com
login <GitHubのユーザー名>
password <ユーザーのパスワード or Personal Access Token>

AWS CLIプロファイルを作成

ユーザーデータ処理でデータベース接続情報をSystems Manager パラメータストアから取得するようにAWS CLIで実行するため、AWS CLIの設定と認証情報が必要となります。
事前にAWS CLIの設定ファイルと認証情報ファイルを作成します。

ファイルをS3にアップロード

これまで作成したファイルを以下のフォルダ構成としてS3にアップロードします。

  • Nginx設定ファイル(mysite.conf)
  • settings_secret.py
  • Dockerfile(APサーバ, Webサーバ)
  • docker-compose.yml
  • .netrc(Github認証情報ファイル)
  • AWS CLIの設定(Config)と認証情報(Credentials)
S3
django-config-docker
├── .netrc
├── config
├── credentials
├── docker-compose.yml
├── django
│   ├── Dockerfile                      <= GunicornコンテナのDockerfile
│   └── settings_secret.py
└── nginx                           
    ├── Dockerfile                      <= NginxコンテナのDockerfile
    └── mysite.conf

構築作業

準備ができたら構築作業を進めていきます。

CloudFormationスタック作成

テンプレートからスタックを作成します。
展開されたEC2インスタンスのディレクトリ構成は以下のようになります。
※一部抜粋して記載しています。

directory
/home/ec2-user/docker-django
├── docker-compose.yml
├── django
│   ├── Dockerfile                      <= GunicornコンテナのDockerfile
│   ├── settings_secret.py
│   └── mysite               <= Djangoアプリケーション
│       ├── manage.py
│       ├── requirements.py
│       └── mysite
│           └── settings.py
└── nginx                           
    ├── Dockerfile                      <= NginxコンテナのDockerfile
    └── mysite.conf

コンテナ展開

Docker Composeを使用してコンテナのビルド、起動をしていきます。
Dockerホストでdocker-compose.ymlが配置されているパスに移動して、イメージのビルドを実行します。

DockerHost
cd ~/docker-django
docker-compose build

ビルドが完了したら、サービスを起動します。
※オプション-dをつけることでバックグランドで実行します。

DockerHost
docker-compose up -d

クライアントPCのブラウザでhttp://<EC2インスタンスのパブリックIP>/adminにアクセスできることを確認します。
しかし、CSSが反映されていない状態となっているかと思います。
これはCSSファイルを含むDjangoアプリケーションの静的ファイルが/public/staticに配置されていないことが原因となります。
以下のコマンドを実行し、静的ファイルを指定の場所に集める処理を実行します。

DockerHost
docker-compose exec gunicorn python3 manage.py collectstatic

これでCSSが反映された状態で管理画面を開くことができるようになりました。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3