티스토리 뷰
개요
이번 포스팅에서는 AWS EKS 환경에 배포되어 있는 어플리케이션과 연동하기 위해 MariaDB를 Kubernetes에 배포해 보자.
- ① AWS EKS 환경에 MariaDB 구축
- ② AWS Code Series 파이프라인 구축
AWS EKS 환경에 MariaDB 구축
- Amazon EFS CIS 드라이버 배포 및 테스트
- Amazon EFS CIS 드라이버 설치
- Amazon EFS CIS 드라이버 배포
- Amazon EFS CSI 드라이버 테스트
- AWS EKS에 MariaDB 설치
- MariaDB 테스트
Amazon EFS CSI 드라이버 배포 및 테스트
CSI 드라이버의 서비스 계정이 AWS API를 호출할 수 있도록 허용하는 IAM ROLE을 생성한다.
1. GitHub에서 IAM 정책 문서 다운로드
curl -o iam-policy-example.json https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/v1.2.0/docs/iam-policy-example.json
2. IAM 정책 생성 (create-policy)
aws iam create-policy \
--policy-name AmazonEKS_EFS_CSI_Driver_Policy \
--policy-document file://iam-policy-example.json
- 생성한 IAM 정책 중 Issuer 확인
aws eks describe-cluster --name your_cluster_name --query "cluster.identity.oidc.issuer" --output text
- your_cluster_name - 클러스터 이름으로 변경
[root@ip-192-168-109-4 ~]# aws eks describe-cluster --name NRSON-EKS-CLUSTER --query "cluster.identity.oidc.issuer" --output text
https://oidc.eks.ap-northeast-2.amazonaws.com/id/abcdefghijklmnopqrstuvwxyz
[root@ip-192-168-109-4 ~]#
3. IAM Trust Policy 생성
- IAM 신뢰 정책을 생성한 다음 Kubernetes 서비스 계정에 AssumeRoleWithWebIdentity 작업 부여
cat <<EOF > trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::AWS_ACCOUNT_ID:oidc-provider/oidc.eks.AWS_DEFAULT_REGION.amazonaws.com/id/<XXXXXXXXXXAAAAAXXXXXXXXXX>"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.AWS_DEFAULT_REGION.amazonaws.com/id/<XXXXXXXXXXAAAAAXXXXXXXXXX>:sub": "system:serviceaccount:kube-system:efs-csi-controller-sa"
}
}
}
]
}
EOF
- AWS_ACCOUNT_ID - account id로 변경
- AWS_DEFAULT_REGION - 해당 AWS 리전으로 변경
- XXXXXXXXXXAAAAAXXXXXXXXXX - 2단계에서 반환된 값으로 변경
4. IAM 역할 생성
aws iam create-role \
--role-name AmazonEKS_EFS_CSI_DriverRole \
--assume-role-policy-document file://"trust-policy.json"
5. 새 IAM 정책을 역할에 연결
aws iam attach-role-policy \
--policy-arn arn:aws:iam::<AWS_ACCOUNT_ID>:policy/AmazonEKS_EFS_CSI_Driver_Policy \
--role-name AmazonEKS_EFS_CSI_DriverRole
- AWS_ACCOUNT_ID - account id로 변경
Amazon EFS CIS 드라이버 설치
1. CIS 드라이버 설치
매니페스트를 다운로드하여 퍼블릭 Amazon ECR 레지스트리에 저장된 이미지를 사용해 드라이버를 설치한다.
$ kubectl kustomize "github.com/kubernetes-sigs/aws-efs-csi-driver/deploy/kubernetes/overlays/stable/?ref=release-1.3" > public-ecr-driver.yaml <br>
2. public-ecr-driver.yaml 편집 (ServiceAccount annotation 추가)
public-ecr-driver.yaml 파일을 편집하고 생성한 IAM 역할의 ARN으로 'efs-csi-controller-sa' Kubernetes ServiceAccount Object에 annotations을 추가한다.
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/name: aws-efs-csi-driver
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<AWS_ACCOUNT_ID>:role/AmazonEKS_EFS_CSI_DriverRole
name: efs-csi-controller-sa
namespace: kube-system
- AWS_ACCOUNT_ID - account id로 변경
Amazon EFS CSI 드라이버를 배포한다. Amazon EFS CSI 드라이버를 사용하면 ReadWriteMany 모드를 사용하여 여러 Pod를 동시에 볼륨에 쓸 수 있다.
3. public-ecr-driver.yaml 배포
$ kubectl apply -f public-ecr-driver.yaml
aws eks describe-cluster --name <CLUSTER_NAME> --query "cluster.resourcesVpcConfig.vpcId" --output text
- CLUSTER_NAME - EKS CLUSTER로 변경
[root@ip-192-168-109-4 EFS]# aws eks describe-cluster --name your_cluster_name --query "cluster.resourcesVpcConfig.vpcId" --output text
vpc-vpcidddddddddd
[root@ip-192-168-109-4 EFS]#
5. VPC 클러스터에 대한 CIDR 범위 확인
aws ec2 describe-vpcs --vpc-ids YOUR_VPC_ID --query "Vpcs[].CidrBlock" --output text
- YOUR_VPC_ID - 4번에서 확인한 vpc id로 변경
[root@ip-192-168-109-4 EFS]# aws ec2 describe-vpcs --vpc-ids vpc-vpcidddddddddd --query "Vpcs[].CidrBlock" --output text
192.168.0.0/16
[root@ip-192-168-109-4 EFS]#
6. Security Group 생성
> Amazon EFS 탑재 지점에 대한 인바운드 네트워크 파일 시스템(NFS) 트래픽을 허용하는 보안 그룹 생성
aws ec2 create-security-group --description efs-test-sg --group-name efs-sg --vpc-id YOUR_VPC_ID
- YOUR_VPC_ID - 4번에서 확인한 vpc id로 변경
7. VPC의 리소스가 Amazon EFS 파일 시스템과 통신할 수 있도록 NFS 인바운드 규칙 추가
aws ec2 authorize-security-group-ingress --group-id sg-xxx --protocol tcp --port 2049 --cidr YOUR_VPC_CIDR
- YOUR_VPC_CIDR - 5번에서 확인한 vpc cidr로 변경
- sg-xxx - 6번에서 생성한 security group id로 변경
8. Amazon EKS 클러스터에 대한 Amazon EFS 파일 시스템 생성
aws efs create-file-system --creation-token eks-efs
참고: 나중에 사용하기 위해 FileSystemId를 저장합니다.
[root@ip-10-192-10-183 EFS]# aws efs create-file-system --creation-token eks-efs
{
"SizeInBytes": {
"ValueInIA": 0,
"ValueInStandard": 0,
"Value": 0
},
"FileSystemArn": "arn:aws:elasticfilesystem:ap-northeast-2:<AWS_ACCOUNT_ID>:file-system/fs-029c93b311a237e4e",
"ThroughputMode": "bursting",
"CreationToken": "eks-efs",
"Encrypted": false,
"Tags": [],
"CreationTime": 1662763838.0,
"PerformanceMode": "generalPurpose",
"FileSystemId": "fs-aaaabbbb00001111",
"NumberOfMountTargets": 0,
"LifeCycleState": "creating",
"OwnerId": "<AWS_ACCOUNT_ID>"
}
[root@ip-10-192-10-183 EFS]#
9. Amazon EFS mount target 생성
aws efs create-mount-target --file-system-id FileSystemId --subnet-id SubnetID --security-group sg-xxx
- FileSystemId - 8번에서 생성한 file-system의 FileSystemId로 변경
- SubnetID - Kubernetes WorkerNode에서 사용하는 Subnet으로 변경
- sg-xxx - 6번에서 생성한 security group id로 변경.
- 여러 서브넷에 mount target을 생성하려면 각 서브넷 ID에 대해 해당 과정을 반복 수행한다. mount targe이 있는 가용 영역의 모든 Amazon Elastic Compute Cloud(Amazon EC2) 인스턴스가 파일 시스템을 사용할 수 있다.
Amazon EFS CSI 드라이버 테스트
지금부터는 동일한 로그 파일에 write하는 두 개의 Pod를 배포하여 Amazon EFS CSI 드라이버를 테스트해 보도록 하자.
1. AWS GitHub에서 aws-efs-csi-driver 리포지토리 복제
git clone https://github.com/kubernetes-sigs/aws-efs-csi-driver.git
2. 작업 디렉터리를 Amazon EFS CSI 드라이버 테스트 파일이 포함된 폴더로 변경한다.
cd aws-efs-csi-driver/examples/kubernetes/multiple_pods/
3. 이전에 생성한 Amazon EFS 파일 시스템 ID를 검색한다.
aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text
4. specs/pv.yaml 파일에서 spec.csi.volumeHandle 값을 이전 단계의 Amazon EFS FileSystemId로 변경한다.
apiVersion: v1
kind: PersistentVolume
metadata:
name: efs-pv
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: efs-sc
csi:
driver: efs.csi.aws.com
volumeHandle: fs-aaaaaaaaaaaaaa
5. 테스트에 필요한 Kubernetes 리소스를 생성한다.
kubectl apply -f specs/
6. 기본 네임 스페이스에 영구 볼륨을 나열하고 default/efs-claim 클레임이 있는 영구 볼륨을 찾는다.
kubectl get pv -w
7. 생성한 pv를 확인한다.
kubectl describe pv efs-pv
8. 두 개의 Pod가 동일한 파일에 데이터를 쓰고 있는지 테스트한다.
kubectl exec -it app1 -- tail /data/out1.txt
kubectl exec -it app2 -- tail /data/out1.txt
AWS EKS에 MariaDB 설치
다음으로 생성한 EFS CIS Driver를 활용하여 MariaDB를 설치해 보도록 하자.1. Persistent Volume 생성
- db-pv.yaml 생성
apiVersion: v1
kind: PersistentVolume
metadata:
name: db-pv-volume
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: efs-sc
csi:
driver: efs.csi.aws.com
volumeHandle: fs-0e018b9e35d8b8c83
- db-pv.yaml 파일 반영
[root@ip-192-168-109-4 specs]# kubectl apply -f db-pv.yaml
persistentvolume/db-pv-volume created
[root@ip-192-168-109-4 specs]#
- kubectl describe pv db-pv-volume
Name: db-pv-volume
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"PersistentVolume","metadata":{"annotations":{},"name":"db-pv-volume"},"spec":{"accessModes":["ReadWriteOnce"],"...
Finalizers: [kubernetes.io/pv-protection]
StorageClass: efs-sc
Status: Available
Claim:
Reclaim Policy: Retain
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 10Gi
Node Affinity: <none>
Message:
Source:
Type: CSI (a Container Storage Interface (CSI) volume source)
Driver: efs.csi.aws.com
VolumeHandle: fs-0e018b9e35d8b8c83
ReadOnly: false
VolumeAttributes: <none>
Events: <none>
2. Persistent Volume Claim 생성
- db-claim.yaml 생성
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-pv-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: efs-sc
resources:
requests:
storage: 10Gi
- db-claim.yaml 파일 반영
[root@ip-192-168-109-4 specs]# kubectl apply -f db-claim.yaml
persistentvolumeclaim/db-pv-claim created
[root@ip-192-168-109-4 specs]#
- kubectl describe pvc db-pv-claim
Name: db-pv-claim
Namespace: default
StorageClass: efs-sc
Status: Bound
Volume: db-pv-volume
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"PersistentVolumeClaim","metadata":{"annotations":{},"name":"db-pv-claim","namespace":"default"},"spec":{"access...
pv.kubernetes.io/bind-completed: yes
pv.kubernetes.io/bound-by-controller: yes
Finalizers: [kubernetes.io/pvc-protection]
Capacity: 10Gi
Access Modes: RWO
VolumeMode: Filesystem
Mounted By: mariadb-66457b6967-7h788
Events: <none>
3. DB 정보 k8s Secret 생성
- DB PASSWORD base64 암호화
[root@ip-192-168-109-4 specs]# echo -n 'DB_PASSWORD' | base64
dbpasswordencryptiondata
[root@ip-192-168-109-4 specs]#
DB_PASSWORD를 base64로 암호화 하여 secret에 저장한다.
- db-secret.yaml 생성
---
apiVersion: v1
kind: Secret
metadata:
name: mariadb-secret
data:
password: dbpasswordencryptiondata
- db-secret.yaml 반영
[root@ip-192-168-109-4 specs]# kubectl apply -f db-secret.yaml
secret/mariadb-secret created
[root@ip-192-168-109-4 specs]#
- secret 조회
[root@ip-192-168-109-4 specs]# kubectl get secret
NAME TYPE DATA AGE
default-token-v242p kubernetes.io/service-account-token 3 10h
mariadb-secret Opaque 1 11m
[root@ip-192-168-109-4 specs]#
- kubectl describe secret mariadb-secret
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-pv-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: efs-sc
resources:
requests:
storage: 10Gi
[root@ip-192-168-109-4 MARIADB]# kubectl describe secret mariadb-secret
Name: mariadb-secret
Namespace: default
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
password: 11 bytes
4. MariaDB k8s Service 생성
- mariadb-svc.yaml 파일 생성
---
apiVersion: v1
kind: Service
metadata:
name: mariadb
spec:
ports:
- nodePort: 30306
port: 3306
protocol: TCP
targetPort: 3306
selector:
app: mariadb
type: NodePort
- mariadb-svc.yaml 반영
[root@ip-192-168-109-4 specs]# kubectl apply -f db-claim.yaml
service/mariadb created
[root@ip-192-168-109-4 specs]#
- kubectl describe service mariadb
Name: mariadb
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=mariadb
Type: ClusterIP
IP: 10.100.173.172
Port: <unset> 3306/TCP
TargetPort: 3306/TCP
Endpoints: 192.168.158.23:3306
Session Affinity: None
Events: <none>
5. MariaDB k8s Deployment 생성
- mariadb-deployment.yaml 파일 생성
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mariadb
spec:
selector:
matchLabels:
app: mariadb
strategy:
type: Recreate
template:
metadata:
labels:
app: mariadb
spec:
containers:
- image: mariadb:10.4
name: mariadb
ports:
- containerPort: 3306
name: mariadb
volumeMounts:
- name: mariadb-persistent-storage
mountPath: /var/lib/mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mariadb-secret
key: password
volumes:
- name: mariadb-persistent-storage
persistentVolumeClaim:
claimName: db-pv-claim
- spec.template.spec.comtainers.env.valueFrom.secretKeyRef의 key: [VALUE]의 VALUE는 secret에 등록한 data.[VALUE] 값을 매핑한다.
- mariadb-svc.yaml 반영
[root@ip-192-168-109-4 MARIADB]# kubectl apply -f mariadb-deployment.yaml
deployment.apps/mariadb created
[root@ip-192-168-109-4 MARIADB]#
- kubectl describe deployment mariadb
Name: mariadb
Namespace: default
CreationTimestamp: Fri, 02 Sep 2022 15:16:11 +0000
Labels: <none>
Annotations: deployment.kubernetes.io/revision: 1
kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"mariadb","namespace":"default"},"spec":{"selector":{"matc...
Selector: app=mariadb
Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType: Recreate
MinReadySeconds: 0
Pod Template:
Labels: app=mariadb
Containers:
mariadb:
Image: mariadb:10.7
Port: 3306/TCP
Host Port: 0/TCP
Environment:
MYSQL_ROOT_PASSWORD: <set to the key 'password' in secret 'mariadb-secret'> Optional: false
Mounts:
/var/lib/mysql from mariadb-persistent-storage (rw)
Volumes:
mariadb-persistent-storage:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: db-pv-claim
ReadOnly: false
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: mariadb-66457b6967 (1/1 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 56m deployment-controller Scaled up replica set mariadb-66457b6967 to 1
- kubectl get pods -l app=mariadb
[root@ip-192-168-109-4 MARIADB]# kubectl get pods -l app=mariadb
NAME READY STATUS RESTARTS AGE
mariadb-66457b6967-7h788 1/1 Running 0 57m
[root@ip-192-168-109-4 MARIADB]#
MariaDB 테스트
1. MariaDB Pod 접속
[root@ip-192-168-109-4 MARIADB]# kubectl exec -it mariadb-66457b6967-7h788 -- bash
root@mariadb-66457b6967-7h788:/#
2. mysql 접속
- pod에 접속하여 직접 mysql client에 접속
root@mariadb-66457b6967-7h788:/# mysql -u root -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 10
Server version: 10.7.5-MariaDB-1:10.7.5+maria~ubu2004 mariadb.org binary distribution
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.020 sec)
MariaDB [(none)]>
Enter password는 secret에 등록한 'DB_PASSWORD'이다.
- remote에서 mariadb-client로 접속 (--mysql -h [POD_IP])
[root@ip-192-168-109-4 specs]# kubectl run -it --rm --image=mariadb:10.7 --restart=Never mariadb-client -- mysql -h 192.168.158.23 -p'DB_PASSWORD'
If you don't see a command prompt, try pressing enter.
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.031 sec)
MariaDB [(none)]>
결론
지금까지 EFS를 활용하여 EKS 환경에 MariaDB를 구축해 보았다. 기업에서는 AWS 환경에서 RDB를 구축, 운영할 때 RDS를 사용할 것인지, 아니면 EC2 인스턴스에 사용 중인 데이터베이스를 직접 설치하는 방식을 선택할 것인지 결정해야 한다.
이를 선택하는 기준은 몇 가지가 있겠지만, 대표적으로 비용, 종속성 (벤더 Lock-In), 유지보수, 업그레이드, 보안 등이 선택의 기준이 된다.
Amazon RDS는 유지보수 효율성, 업그레이드 편의성 측면에서 강력한 기능을 지원하며, 특히 온프레미스 RDS를 AWS 환경 내 마이그레이션할 수 있는 도구도 다양하게 지원한다. (MySQL, Oracle, SQL Server, PostgreSQL, MariaDB, Aurora(MySQL과 호환) 등 지원)
EC2에 RDB를 직접 설치하는 경우 비용 측면에서 확연히 강점이 있다. 또한, 타 클라우드 환경으로 이전할 경우 손쉽게 이전이 가능하다. 다만, 기 구축되어 있는 RDB가 존재하고 이를 운영하는 조직이 있으며, 데이터 사이즈가 크지 않을 경우에는 EC2 인스턴스에 구축하는 것이 비용을 절감할 수 있겠지만, 신규 구축되는 조직의 경우 운영 유지보수를 직접 수행 할 경우 반대의 효과가 날수도 있다.
또한, 보안 측면에서 강력한 RDS이지만, 때로는 기업에서 관리하는 소프트웨어를 설치하여 운영해야 하는 경우도 발생할 수 있다. 특히 이는 보안 뿐만 아니라 메트릭을 측정하는 솔루션이거나, 알람을 알려주는 솔루션 등 시스템 통합 측면의 솔루션들의 유연한 관리가 필요할 수 있다. 이 경우 RDS보다는 EC2 기반의 관리가 효율적일 수 있다.
인프라 운영을 위한 어플리케이션, 제대로 설계된 자동화 그리고 데이터베이스 전문 관리 팀이 있는 기업의 경우 RDS가 꼭 필요하지 않을 수 있다. 따라서 반드시 라기보다는 RDS의 기능, 장단점 그리고 비용 요소를 정확히 파악하여 구축 방법에 대해 결정하는 것이 바람직할 것이다.
# 참조 (StatefulSet vs Deployment)
- StatefulSet 적용이 용이한 환경
- IP 변경없이 직접 접근해야 하는 서비스 (Database, Redis, Kafka 등)
- 데이터 유실이 발생하지 않아야 하는 서비스 (데이터 영속성을 유지해야 하는 경우)
- 빈번한 업데이트와 다운타임이 발생되지 않아야 하는 중요한 서비스 (Rolling Update)
- StatefulSet의 특징
- 스토리지 영속성을 유지하기 위해 Pod의 Storage는 PersistentVolume 또는 StorageClass로 프로비저닝해야 함
- Pod의 네트워크ID를 유지하기 위해 headless service 필요
# 참조 (Headless Service)
Headless service란 .spec.clusterIP가 None으로 구성된 서비스를 의미한다. 서비스의 로드밸런싱 기능을 수행하지 않는 서비스이다. StatefulSet의 경우 Pod 별 개별 네트워크가 구성되어 있고 이와 통신하기 위한 DNS 정보를 할당하기 위한 용도로 Headless Service가 사용된다.
Pod DNS는 POD_NAME.SERVICE_NAME.NAMESPACE_NAME.svc.DNS_NAME으로 구성되며, 일반적으로는 mariadbpoda.mariadb.default.svc.cluster.local로 DNS가 구성된다. 따라서 이와 같은 DNS 정보를 유지하기 위해 Service 생성이 필요하다.
'③ 클라우드 > ⓐ AWS' 카테고리의 다른 글
AWS CodeSeries 파이프라인 구성하기 (buildspec.yml) (0) | 2022.09.11 |
---|---|
Amazon EKS 제로 다운타임 배포환경 구현하기 (1) | 2021.01.17 |
AWS Backup을 활용하여 EC2 백업 관리하기 (0) | 2020.12.11 |
Amazon Cognito를 이용한 OIDC 인증/인가 프로세스 (3) | 2020.11.17 |
Oracle DB AWS 환경에서 관리하기 - Amazon RDS (0) | 2020.11.10 |
- Total
- Today
- Yesterday
- 쿠버네티스
- OpenStack
- SWA
- openstack tenant
- k8s
- JBoss
- nodejs
- aws
- Docker
- MSA
- API Gateway
- node.js
- TA
- aa
- 오픈스택
- git
- Da
- webtob
- 아키텍처
- SA
- wildfly
- Architecture
- apache
- openstack token issue
- jeus
- JEUS6
- 마이크로서비스 아키텍처
- kubernetes
- 마이크로서비스
- JEUS7
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |