2022/2/5 環境変数ファイルをユーザーデータ内で作成するように変更
#はじめに
以前EC2+ELB+RDSでDjango環境を構築してみましたが、今回はDockerコンテナでDjango環境を構築してみたいと思います。
今回はコンテナを使用して構築することが主目的となるため、ELBやマルチAZを使用したスケーラブルな構成は考慮しません。
以下の記事を参考にさせていただきました。
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
を作成しそちらに情報を転記します。
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
の修正を行います。
※修正部分のみ抜粋して記載します。
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.conf
のhttp
ディレクティブ内のinclude /etc/nginx/conf.d/*.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
# 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コンテナ
# 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
の設定内容は以下となります。
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バケットの名前は置き換えて使用してください。
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の内容は以下のようになります。
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」というファイルを作成することで、ファイルに記述した認証情報を使用することができます。
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)
django-config-docker
├── .netrc
├── config
├── credentials
├── docker-compose.yml
├── django
│ ├── Dockerfile <= GunicornコンテナのDockerfile
│ └── settings_secret.py
└── nginx
├── Dockerfile <= NginxコンテナのDockerfile
└── mysite.conf
#構築作業
準備ができたら構築作業を進めていきます。
###CloudFormationスタック作成
テンプレートからスタックを作成します。
展開されたEC2インスタンスのディレクトリ構成は以下のようになります。
※一部抜粋して記載しています。
/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
が配置されているパスに移動して、イメージのビルドを実行します。
cd ~/docker-django
docker-compose build
ビルドが完了したら、サービスを起動します。
※オプション-d
をつけることでバックグランドで実行します。
docker-compose up -d
クライアントPCのブラウザでhttp://<EC2インスタンスのパブリックIP>/admin
にアクセスできることを確認します。
しかし、CSSが反映されていない状態となっているかと思います。
これはCSSファイルを含むDjangoアプリケーションの静的ファイルが/public/static
に配置されていないことが原因となります。
以下のコマンドを実行し、静的ファイルを指定の場所に集める処理を実行します。
docker-compose exec gunicorn python3 manage.py collectstatic
これでCSSが反映された状態で管理画面を開くことができるようになりました。