AWS VPC를 이용해 EC2와 RDS를 안전하게 관리하기
※ AWS VPC 서비스를 이용해 AWS EC2와 RDS를 관리하는 방법 ※
이번 시간에는 AWS VPC 서비스를 이용해 AWS EC2와 RDS를 안전하게 관리하는 방법에 대해서 알아보도록 하겠습니다. 우리는 일반적으로 AWS EC2를 이용해 웹 서버를 구동시키고, MySQL과 같은 데이터베이스는 AWS RDS 서비스를 이용하여 별도의 독립된 공간에서 관리합니다. 이 때 EC2는 웹 서버가 있다는 점에서 퍼블릭(Public) 상태여야 하며 RDS는 EC2에서만 접근할 수 있는 프라이빗(Private) 상태여야 해요.
다시 말해 다음과 같이 시스템을 구성하는 것이 안전하며 효과적입니다.
말 그대로 외부에서는 EC2에만 접근할 수 있으며 AWS RDS 데이터베이스는 EC2를 거쳤을 때만 이용할 수 있습니다. 그래서 외부에서 바로 DB에 접근할 수 없다는 점에서 중요한 정보를 안전하게 보관할 수 있다는 것입니다. 이는 가장 간단하면서도 보편적인 시스템 구성도입니다. 결과적으로 VPC라는 것을 이용해 AWS EC2와 AWS RDS를 감싸야 합니다. 따라서 가장 먼저 AWS VPC를 만들어 보도록 하겠습니다.
※ VPC 만들어보기 ※
▶ VPC 관리: https://console.aws.amazon.com/vpc/
VPC는 해당 사이트에 접속해서 생성할 수 있습니다. 바로 [VPC 만들기] 버튼을 눌러 이동합니다.
우리는 2번째 옵션인 [퍼블릭 및 프라이빗 서브넷이 있는 VPC]를 선택하면 됩니다. 외부에서 접속 가능한 퍼블릭(Public) 서브넷을 반드시 열어 두어야 합니다. 이후에 Public Subnet이라는 이름으로 외부에서 접속 가능한 서브넷을 만들 예정입니다.
다음과 같이 퍼블릭 서브넷(Public Subnet)과 프라이빗 서브넷(Private Subnet)의 이름을 설정하면 됩니다. 이 때 프라이빗 서브넷의 가용 영역을 기억해두시면 좋습니다. 나중에 프라이빗 서브넷을 하나 더 만드는데, 그 프라이빗 서브넷의 설정에 있어서 필요한 정보이기 때문입니다. 그리고 NAT 게이트웨이로 NAT 인스턴스를 사용하시면 됩니다. 어차피 저는 NAT 인스턴스 사용 설정만 해 놓고, 실제로는 사용하지 않을 생각입니다.
그러면 NAT 인스턴스의 세부 정보를 설정할 수 있습니다. 저는 가장 기본적인 인스턴스인 t2.nano를 이용했습니다. 어차피 NAT 인스턴스는 지운 다음에 다시 만들어 줄 생각이에요. 이후에 [VPC 만들기] 버튼을 눌러서 VPC를 생성하시면 됩니다.
이후에 [서브넷] 탭으로 이동해 보시면 다음과 같이 방금 우리가 만들었던 Public Subnet과 Private Subnet이 존재하는 것을 확인할 수 있습니다. 다만 VPC의 정상적인 동작을 위해서는 한 개의 Private Subnet을 더 만들어야 합니다. 따라서 [서브넷 생성] 버튼을 누를 수 있습니다.
서브넷을 생성할 때는 아까 우리가 생성했던 VPC를 대상 VPC로 설정해주시면 됩니다.
다만 이번에 만드는 두 번째 프라이빗 서브넷은 처음에 만들었던 프라이빗 서브넷과 다른 [가용 영역]을 사용해야 합니다. 만약 같은 가용 영역을 사용하시는 경우, 나중에 최종 단계에서 가용 영역이 최소 2개 이상 필요하다는 오류 메시지를 만나게 될 거예요. 또한 IPv4 CIDR 블록으로는 10.0.2.0/24를 사용하시면 됩니다. 이후에 [생성]을 진행해주시면 됩니다.
이제 다음과 같이 Private Subnet이 2개 존재하는 것을 확인할 수 있습니다. 이 두 개의 프라이빗 서브넷이 동일한 라우팅 테이블을 가지는지 확인해주시면 됩니다. 다음 사진과 같이 각 서브넷을 클릭해서 [라우팅 테이블] 탭에서 확인하시면 됩니다.
※ 보안 그룹 설정하기 ※
이제 실제로 어떠한 프로토콜을 이용해서 우리의 VPC에 접근할 수 있는지를 설정하기 위해서 [보안 그룹] 탭으로 이동해서 [Create Security Group] 버튼을 눌러서 보안 그룹을 만들어 주시면 됩니다.
기본적으로 외부에서 접속이 가능한 보안 그룹으로 Security Group을 생성합니다. 마찬가지로 대상 VPC로는 처음에 만들었던 VPC를 넣어주신 뒤에 [Create] 버튼을 눌러서 생성해주시면 됩니다.
보안 그룹을 만든 이후에는 다음과 같이 만들어진 보안 그룹을 클릭하여 [Edit Rules] 버튼을 눌러서 규칙(Rule)을 추가하시면 됩니다.
이후에 다음과 같이 SSH와 HTTP 프로토콜을 열어서 외부에서 접속할 수 있도록 설정하시면 됩니다.
결과적으로 다음 그림과 같이 인바운드 규칙(Inbound Rule)을 확인해 보시면 80번 포트 및 22번 포트로는 외부에서 접속이 가능한 것을 확인할 수 있습니다. 이제 내부망에서 DB에 접속할 수 있는 보안 그룹 또한 만들어주도록 하겠습니다.
이후에 다음과 같이 DB Security Group이라는 이름으로 보안 그룹을 만든 뒤에 대상 VPC를 설정해주시면 됩니다.
이번에 만들어진 보안 그룹도 마찬가지로 [Edit Rules]를 눌러서 규칙을 추가할 수 있습니다. 이 때, 아까 만들었든 Security Group의 그룹 ID를 저장해 두시면 됩니다.
다음과 같이 3306 포트로만 접속할 수 있도록 설정하시면 됩니다. 이 때 [소스] 탭으로는 아까 만들었던 Security Group의 그룹 ID를 넣으시면 됩니다. 그러면 Security Group에 속해 있는 컴퓨터만 DB Security Group에 접근할 수 있게 됩니다.
※ 데이터베이스 서브넷 설정 ※
이제 하나의 데이터베이스를 만들어서 설정을 해보도록 하겠습니다. 가장 먼저 [서브넷 그룹] 탭으로 이동해서 [DB 서브넷 그룹 생성] 버튼을 누르시면 됩니다.
이후에 다음과 같이 DB 서브넷 그룹을 만들어 주세요. 만드실 때는 처음에 생성했던 VPC로 설정하시고 [이 VPC와 관련된 모든 서브넷 추가] 버튼을 눌러서 VPC와 관련된 서브넷을 모두 추가하시면 됩니다.
결과적으로 모든 서브넷이 추가된 것을 확인할 수 있습니다.
※ 데이터베이스 만들기 ※
결과적으로 [데이터베이스] 탭으로 이동해서 [데이터베이스 생성] 버튼을 눌러서 데이터베이스를 만들어 보도록 하겠습니다.
다음과 같이 프리 티어로 MySQL을 만들어 보도록 하겠습니다.
이후에 다음과 같이 DB 세부 정보를 지정할 수 있습니다. 저는 MySQL 5.6으로 설정하고 기본 설정 그대로 진행했습니다.
이후에 고급 설정 탭에서 VPC의 세부 설정을 진행하시면 됩니다. 다음과 같이 아까 만들어 주었던 DB Subnet Group과 DB Security Group으로 설정을 해주시면 됩니다. 그러면 동일한 VPC에 있는 Security Group에서만 이 DB에 접근할 수 있게 됩니다.
이후에 나머지 설정을 해주시고 DB 인스턴스를 만들어 주시면 됩니다.
※ AWS EC2 인스턴스 만들기 ※
이제 AWS EC2 인스턴스를 만들어서 퍼블릭(Public)한 접근이 가능하도록 설정하겠습니다. 따라서 AWS EC2 관리 페이지의 [인스턴스] 탭에서 [인스턴스 시작] 버튼을 눌러 인스턴스를 만들어 보도록 하겠습니다.
저는 우분투(Ubuntu) 서버로 생성을 진행하여 인스턴스까지 설정해보도록 하겠습니다.
이후에 인스턴스 세부 정보를 편집하여 VPC 설정을 진행하도록 하겠습니다.
다음과 같이 처음에 만들었던 VPC로 설정을 진행하시면 됩니다. 또한 서브넷 설정에 있어서 만들었던 Public Subnet으로 설정하시면 됩니다.
이후에 [보안 그룹 구성] 탭으로 이동하여 아까 만들었던 Security Group으로 설정하여 외부에서 접근 가능하도록 설정합니다.
이후에 인스턴스를 시작해주시면 됩니다.
또한 해당 인스턴스에 접근하기 위하여 키 페어를 설정합니다.
이제 인스턴스가 생성되었습니다.
다만 이렇게 만들어진 인스턴스는 기본적으로 공인 IP가 할당되어 있지 않습니다. 따라서 탄력적 IP(Elastic IP)를 설정해줍니다.
하나의 탄력적 IP를 선택해 [주소 연결] 기능을 눌러 다음과 같이 방금 생성했던 인스턴스를 설정해주시면 됩니다.
이제 이 인스턴스에 해당 퍼블릭 IP로 접속할 수 있습니다.
실제로 접속을 해보면 다음과 같습니다.
※ VPC 내부의 MySQL 접속하기 ※
이제 VPC 내부에 있는 MySQL에 접속해보도록 하겠습니다. AWS RDS 서비스로 가서 아까 만들었던 DB를 확인합니다. 그러면 해당 DB에 접속할 수 있는 URL이 보입니다.
이제 외부에서 여기에 접속하려고 하면 접속 실패 메시지가 뜨는 것을 확인할 수 있습니다.
따라서 EC2에서 MySQL을 설치해서 EC2에서 해당 MySQL 서버에 접속해보도록 하겠습니다.
▶ APT 업데이트: sudo apt-get update
▶ MySQL 설치: sudo apt install mysql-client-core-5.7
다음 사진과 같이 동일 VPC 내의 EC2에서 접속했을 때 비로소 접속이 되는 것을 확인할 수 있습니다.
▶ MySQL 접속하기: sudo mysql -u user -p --port 3306 --host db.crtzcfwok2ed.ap-northeast-2.rds.amazonaws.com
이로써 VPC 내부에서만 접속이 가능한 MySQL 데이터베이스가 구축이 되었습니다. 반면에 EC2는 퍼블릭(Public)한 공간에 있기 때문에 누구나 언제든지 접속이 가능합니다. 이와 같은 구성이 많이 사용되는 이유는 EC2에 MySQL을 설치하는 것보다 RDS 서비스로 따로 분리하는 것이 더 효과적인 DB 관리가 가능하기 때문이에요.
'기타' 카테고리의 다른 글
| AWS EC2에 AWS RDS 연동하기 (3) | 2019.01.03 |
|---|---|
| AWS VPC 안에 포함되어 있는 서비스들을 통째로 제거하는 방법 (2) | 2019.01.03 |
| 노션(Notion)을 활용한 팀 프로젝트 관리 (0) | 2018.12.31 |
| Freenom 서비스로 무료 도메인 호스팅 이용하기! (2) | 2018.12.31 |
| Xshell 6 개인 무료 라이센스로 설치하여 사용하는 방법 (0) | 2018.12.31 |
13강 Express에서 파일 업로드 요청 처리 및 DB에 데이터 삽입 [React와 Node.js로 만드는 고객 관리 시스템 개발 강좌]
이번 시간에는 사용자가 보낸 고객 데이터를 Node.js Express로 처리하여 서버의 특정한 폴더에 파일 업로드 처리를 하는 방법에 대해서 알아보도록 하겠습니다. 이후에 고객 정보를 실제 MySQL 데이터베이스(Database)에 삽입하고, 업로드 폴더를 클라이언트에서 접근할 수 있도록 하여 고객이 이를 확인할 수 있도록 처리하는 시간을 가져 볼 것입니다.
▶ CustomerAdd.js
따라서 가장 먼저 CustomerAdd.js의 handleFormSubmit() 함수를 수정해주도록 합시다. 데이터를 전송한 이후에는 고객 추가 양식(Form)을 비운 뒤에 페이지를 새로고침(Refresh)하여 등록된 고객 데이터를 확인하는 것입니다. 실제 배포 버전에서는 전체 페이지를 새로고침 하는 방향으로 코딩을 하면 안 되지만 빠른 테스트를 위해서 잠시 이와 같이 코딩해주도록 하겠습니다.
handleFormSubmit(e) {
e.preventDefault()
this.addCustomer()
.then((response) => {
console.log(response.data);
})
this.setState({
file: null,
userName: '',
birthday: '',
gender: '',
job: '',
})
window.location.reload();
}
이제 node.js에서 이러한 파일 업로드 요청을 처리하시면 됩니다. 이 때는 multer 라이브러리를 사용하시면 됩니다. 따라서 루트 폴더로 이동하셔서 npm install --save multer 명령어를 수행하신 뒤에 다음 소스코드를 작성합니다.
▶ server.js
const fs = require('fs');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = process.env.PORT || 5000;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const data = fs.readFileSync('./database.json');
const conf = JSON.parse(data);
const mysql = require('mysql');
const multer = require('multer');
const upload = multer({dest: './upload'})
const connection = mysql.createConnection({
host: conf.host,
user: conf.user,
password: conf.password,
port: conf.port,
database: conf.database
});
connection.connect();
app.get('/api/customers', (req, res) => {
connection.query(
'SELECT * FROM CUSTOMER',
(err, rows, fields) => {
res.send(rows);
}
)
});
app.use('/image', express.static('./upload'));
app.post('/api/customers', upload.single('image'), (req, res) => {
let sql = 'INSERT INTO CUSTOMER VALUES (null, ?, ?, ?, ?, ?)';
let image = '/image/' + req.file.filename;
let name = req.body.name;
let birthday = req.body.birthday;
let gender = req.body.gender;
let job = req.body.job;
let params = [image, name, birthday, gender, job];
connection.query(sql, params,
(err, rows, fields) => {
res.send(rows);
}
)
});
app.listen(port, () => console.log(`Listening on port ${port}`));
이제 루트 폴더에서 upload 폴더를 생성해 주세요. 또한 사용자가 업로드한 파일은 깃 허브(Git Hub)에 공개적으로 올라가면 안 된다는 점에서 .gitignore에도 이 내용을 추가해주도록 합니다.
# upload
/upload
※ 실행 결과 ※
실제로 고객 데이터와 파일이 업로드 된 이후에 새로고침이 자동으로 이루어지면서 추가된 고객 정보를 확인할 수 있습니다.
결과적으로 다음과 같이 upload 폴더에서도 업로드 된 파일을 확인할 수 있습니다. 파일의 이름은 multer 라이브러리에 의해서 중복되지 않는 형태로 자동으로 바뀌어서 올라가게 됩니다.
(+ 추가)
실제로는 이러한 이미지 업로드 기능은 AWS S3과 같은 서비스를 이용해서 저장하면 매우 효과적입니다. 현재 소스코드 상으로는 데이터베이스와 서버에 업로드 된 이미지가 완전히 일치한다는 것을 보장하기 어렵기 때문입니다. 그리고 현재 소스코드에서는 새로운 고객 데이터가 등록된 이후에, 이를 확인하기 위해 전체 페이지를 새로고침(Refresh)하지만 실제로는 다시 고객 목록 데이터를 가져오는 식으로 코드를 동작시켜야 합니다.
'React와 Node.js로 만드는 고객 관리 시스템 개발 강좌' 카테고리의 다른 글
12강 고객 추가 양식(Form) 구현하기 [React와 Node.js로 만드는 고객 관리 시스템 개발 강좌]
React에서 Form을 처리하기 위해서는 이벤트 처리(Event Handling)를 수행해야 합니다. 이번 시간에는 그러한 이벤트 처리 방법에 대해서 이해하고, 결과적으로 서버로 고객 데이터를 전송하여 데이터베이스에 신규 고객 정보를 등록하는 방법까지 알아보도록 하겠습니다. 따라서 가장 먼저 React 클라이언트의 components 폴더에 CustomerAdd.js를 만들어 보도록 하겠습니다.
이제 디자인을 입히지 않은 상태로 하나의 <form> 태그를 작업하는 시간을 가져보도록 하겠습니다. 이를 위해 가장 먼저 서버와의 통신 목적의 라이브러리인 axios를 설치해주도록 하겠습니다. client 폴더로 이동하여 npm install --save axios 명령어를 입력하시면 됩니다. 이제 CustomerAdd.js 컴포넌트를 작업해 줍니다.
▶ CustomerAdd.js
import React from 'react'
import { post } from 'axios';
class CustomerAdd extends React.Component {
constructor(props) {
super(props);
this.state = {
file: null,
userName: '',
birthday: '',
gender: '',
job: '',
fileName: ''
}
this.handleFormSubmit = this.handleFormSubmit.bind(this)
this.handleFileChange = this.handleFileChange.bind(this)
this.handleValueChange = this.handleValueChange.bind(this)
this.addCustomer = this.addCustomer.bind(this)
}
handleFormSubmit(e) {
e.preventDefault()
this.addCustomer()
.then((response) => {
console.log(response.data);
})
}
handleFileChange(e) {
this.setState({
file: e.target.files[0],
fileName: e.target.value
});
}
handleValueChange(e) {
let nextState = {};
nextState[e.target.name] = e.target.value;
this.setState(nextState);
}
addCustomer(){
const url = '/api/customers';
const formData = new FormData();
formData.append('image', this.state.file)
formData.append('name', this.state.userName)
formData.append('birthday', this.state.birthday)
formData.append('gender', this.state.gender)
formData.append('job', this.state.job)
const config = {
headers: {
'content-type': 'multipart/form-data'
}
}
return post(url, formData, config)
}
render() {
return (
<form onSubmit={this.handleFormSubmit}>
<h1>고객 추가</h1>
프로필 이미지: <input type="file" name="file" file={this.state.file} value={this.state.fileName} onChange={this.handleFileChange} /><br/>
이름: <input type="text" name="userName" value={this.state.userName} onChange={this.handleValueChange} /><br/>
생년월일: <input type="text" name="birthday" value={this.state.birthday} onChange={this.handleValueChange} /><br/>
성별: <input type="text" name="gender" value={this.state.gender} onChange={this.handleValueChange} /><br/>
직업: <input type="text" name="job" value={this.state.job} onChange={this.handleValueChange} /><br/>
<button type="submit">추가하기</button>
</form>
)
}
}
export default CustomerAdd
▶ App.js
이제 작업한 CustomerAdd 컴포넌트를 화면에 출력해 보도록 하겠습니다. 따라서 App.js를 수정해 봅시다.
import CustomerAdd from './components/CustomerAdd';
위와 같이 CustomerAdd 컴포넌트를 추가해주신 이후에 다음과 같이 렌더링 되는 부분을 수정합니다.
<div>
<Paper className={classes.root}>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>번호</TableCell>
<TableCell>이미지</TableCell>
<TableCell>이름</TableCell>
<TableCell>생년월일</TableCell>
<TableCell>성별</TableCell>
<TableCell>직업</TableCell>
</TableRow>
</TableHead>
<TableBody>
{this.state.customers ?
this.state.customers.map(c => {
return <Customer key={c.id} id={c.id} image={c.image} name={c.name} birthday={c.birthday} gender={c.gender} job={c.job} />
}) :
<TableRow>
<TableCell colSpan="6" align="center">
<CircularProgress className={classes.progress} variant="determinate" value={this.state.completed} />
</TableCell>
</TableRow>
}
</TableBody>
</Table>
</Paper>
<CustomerAdd/>
</div>
실제로 이를 브라우저의 개발자 도구에서 테스트를 해보시면 서버로 데이터를 전송하는 것을 알 수 있습니다.
아래쪽에 고객 추가 양식이 존재하며 여기에 데이터를 입력하여 실제로 고객을 추가할 수 있습니다.
데이터를 입력해 [추가하기] 버튼을 누른 결과 다음과 같이 서버로 데이터가 전송되는 것을 알 수 있습니다. 실제로 이미지 파일은 바이너리 코드 형태로 서버로 전달됩니다. 따라서 다음 시간에 이러한 사용자의 요청 데이터를 적절히 처리하여 데이터베이스에 등록한 뒤에 그에 맞는 응답을 보내주는 작업을 수행해야 하빈다.
